summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAchilleas Pipinellis <axilleas@axilleas.me>2016-03-18 14:31:33 +0200
committerAchilleas Pipinellis <axilleas@axilleas.me>2016-03-18 14:31:33 +0200
commit4d3e8ceea562683a8ee3a87a45ece6c476558446 (patch)
treea83c195a72ac7e32edb4888e733792302c13505f
parent11dda8db29a4843026464c0a61f65ada20646e3b (diff)
parentdadd28e317ace1e3d3a2a02926eb352832b97f08 (diff)
downloadgitlab-ce-4d3e8ceea562683a8ee3a87a45ece6c476558446.tar.gz
Merge branch 'master' into docs_select_version_to_install
-rw-r--r--.csscomb.json16
-rw-r--r--.gitignore3
-rw-r--r--.gitlab-ci.yml224
-rw-r--r--.ruby-version2
-rw-r--r--.scss-lint.yml158
-rw-r--r--CHANGELOG346
-rw-r--r--CONTRIBUTING.md216
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile79
-rw-r--r--Gemfile.lock298
-rw-r--r--PROCESS.md115
-rw-r--r--Procfile4
-rw-r--r--README.md2
-rwxr-xr-x[-rw-r--r--]Rakefile3
-rw-r--r--VERSION2
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/OFL.txt7
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttfbin289364 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttf.woffbin0 -> 113800 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttf.woff2bin0 -> 82052 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttfbin103404 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttf.woffbin0 -> 49704 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2bin0 -> 34812 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BlackItalic.ttfbin116360 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttfbin291424 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttf.woffbin0 -> 117872 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttf.woff2bin0 -> 85604 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttfbin103608 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttf.woffbin0 -> 50608 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2bin0 -> 35864 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BoldItalic.ttfbin116192 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttfbin291652 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttf.woffbin0 -> 114336 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2bin0 -> 82808 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttfbin104768 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woffbin0 -> 49684 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2bin0 -> 34560 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-ExtraLightItalic.ttfbin117140 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttfbin104236 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttf.woffbin0 -> 51012 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttf.woff2bin0 -> 36016 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Italic.ttfbin117328 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttfbin293220 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttf.woffbin0 -> 118284 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttf.woff2bin0 -> 86336 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttfbin104616 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttf.woffbin0 -> 50992 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttf.woff2bin0 -> 35952 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-LightItalic.ttfbin116960 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttfbin293956 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttf.woffbin0 -> 119064 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttf.woff2bin0 -> 86844 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttfbin292404 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttf.woffbin0 -> 118412 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttf.woff2bin0 -> 86196 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttfbin104020 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woffbin0 -> 50924 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2bin0 -> 35984 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-SemiboldItalic.ttfbin116424 -> 0 bytes
-rw-r--r--app/assets/images/auth_buttons/azure_64.pngbin0 -> 986 bytes
-rw-r--r--app/assets/images/emoji.pngbin832902 -> 263533 bytes
-rw-r--r--app/assets/images/emoji@2x.pngbin0 -> 690504 bytes
-rw-r--r--app/assets/javascripts/activities.js.coffee18
-rw-r--r--app/assets/javascripts/admin.js.coffee15
-rw-r--r--app/assets/javascripts/api.js.coffee17
-rw-r--r--app/assets/javascripts/application.js.coffee92
-rw-r--r--app/assets/javascripts/autosave.js.coffee6
-rw-r--r--app/assets/javascripts/awards_handler.coffee95
-rw-r--r--app/assets/javascripts/behaviors/autosize.js.coffee22
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js.coffee47
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee1
-rw-r--r--app/assets/javascripts/branch-graph.js.coffee2
-rw-r--r--app/assets/javascripts/breakpoints.coffee37
-rw-r--r--app/assets/javascripts/broadcast_message.js.coffee22
-rw-r--r--app/assets/javascripts/build_artifacts.js.coffee14
-rw-r--r--app/assets/javascripts/calendar.js.coffee5
-rw-r--r--app/assets/javascripts/ci/build.coffee13
-rw-r--r--app/assets/javascripts/commits.js.coffee66
-rw-r--r--app/assets/javascripts/dashboard.js.coffee3
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee25
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee16
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee8
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee276
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee16
-rw-r--r--app/assets/javascripts/issue.js.coffee29
-rw-r--r--app/assets/javascripts/issue_status_select.js.coffee11
-rw-r--r--app/assets/javascripts/issues.js.coffee33
-rw-r--r--app/assets/javascripts/labels_select.js.coffee109
-rw-r--r--app/assets/javascripts/logo.js.coffee50
-rw-r--r--app/assets/javascripts/markdown_preview.js.coffee46
-rw-r--r--app/assets/javascripts/merge_request.js.coffee39
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee22
-rw-r--r--app/assets/javascripts/merge_requests.js.coffee4
-rw-r--r--app/assets/javascripts/milestone.js.coffee23
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee74
-rw-r--r--app/assets/javascripts/notes.js.coffee234
-rw-r--r--app/assets/javascripts/pager.js.coffee3
-rw-r--r--app/assets/javascripts/profile.js.coffee16
-rw-r--r--app/assets/javascripts/project.js.coffee16
-rw-r--r--app/assets/javascripts/project_find_file.js.coffee125
-rw-r--r--app/assets/javascripts/project_new.js.coffee13
-rw-r--r--app/assets/javascripts/project_select.js.coffee3
-rw-r--r--app/assets/javascripts/projects_list.js.coffee53
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee20
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js.coffee19
-rw-r--r--app/assets/javascripts/shortcuts_issuable.coffee31
-rw-r--r--app/assets/javascripts/sidebar.js.coffee24
-rw-r--r--app/assets/javascripts/star.js.coffee4
-rw-r--r--app/assets/javascripts/subscription.js.coffee34
-rw-r--r--app/assets/javascripts/todos.js.coffee56
-rw-r--r--app/assets/javascripts/user.js.coffee11
-rw-r--r--app/assets/javascripts/user_tabs.js.coffee146
-rw-r--r--app/assets/javascripts/users_select.js.coffee88
-rw-r--r--app/assets/javascripts/wikis.js.coffee24
-rw-r--r--app/assets/javascripts/zen_mode.js.coffee104
-rw-r--r--app/assets/stylesheets/application.scss6
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/avatar.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss50
-rw-r--r--app/assets/stylesheets/framework/buttons.scss125
-rw-r--r--app/assets/stylesheets/framework/calendar.scss42
-rw-r--r--app/assets/stylesheets/framework/callout.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss154
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss348
-rw-r--r--app/assets/stylesheets/framework/files.scss62
-rw-r--r--app/assets/stylesheets/framework/filters.scss27
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/fonts.scss24
-rw-r--r--app/assets/stylesheets/framework/forms.scss52
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss30
-rw-r--r--app/assets/stylesheets/framework/header.scss49
-rw-r--r--app/assets/stylesheets/framework/highlight.scss26
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss20
-rw-r--r--app/assets/stylesheets/framework/jquery.scss33
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss35
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss29
-rw-r--r--app/assets/stylesheets/framework/mixins.scss43
-rw-r--r--app/assets/stylesheets/framework/mobile.scss17
-rw-r--r--app/assets/stylesheets/framework/nav.scss142
-rw-r--r--app/assets/stylesheets/framework/pagination.scss28
-rw-r--r--app/assets/stylesheets/framework/panels.scss12
-rw-r--r--app/assets/stylesheets/framework/progress.scss5
-rw-r--r--app/assets/stylesheets/framework/selects.scss149
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss95
-rw-r--r--app/assets/stylesheets/framework/tables.scss13
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss66
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss36
-rw-r--r--app/assets/stylesheets/framework/typography.scss46
-rw-r--r--app/assets/stylesheets/framework/variables.scss144
-rw-r--r--app/assets/stylesheets/framework/zen.scss103
-rw-r--r--app/assets/stylesheets/highlight/dark.scss56
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss40
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss40
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss40
-rw-r--r--app/assets/stylesheets/highlight/white.scss140
-rw-r--r--app/assets/stylesheets/pages/admin.scss10
-rw-r--r--app/assets/stylesheets/pages/appearances.scss11
-rw-r--r--app/assets/stylesheets/pages/awards.scss210
-rw-r--r--app/assets/stylesheets/pages/builds.scss25
-rw-r--r--app/assets/stylesheets/pages/commit.scss5
-rw-r--r--app/assets/stylesheets/pages/commits.scss68
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss10
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss13
-rw-r--r--app/assets/stylesheets/pages/diff.scss127
-rw-r--r--app/assets/stylesheets/pages/editor.scss4
-rw-r--r--app/assets/stylesheets/pages/emojis.scss3002
-rw-r--r--app/assets/stylesheets/pages/events.scss24
-rw-r--r--app/assets/stylesheets/pages/explore.scss8
-rw-r--r--app/assets/stylesheets/pages/graph.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss28
-rw-r--r--app/assets/stylesheets/pages/import.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss187
-rw-r--r--app/assets/stylesheets/pages/issues.scss51
-rw-r--r--app/assets/stylesheets/pages/labels.scss28
-rw-r--r--app/assets/stylesheets/pages/login.scss14
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss51
-rw-r--r--app/assets/stylesheets/pages/milestone.scss53
-rw-r--r--app/assets/stylesheets/pages/note_form.scss15
-rw-r--r--app/assets/stylesheets/pages/notes.scss48
-rw-r--r--app/assets/stylesheets/pages/notifications.scss18
-rw-r--r--app/assets/stylesheets/pages/profile.scss152
-rw-r--r--app/assets/stylesheets/pages/projects.scss384
-rw-r--r--app/assets/stylesheets/pages/runners.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss12
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss4
-rw-r--r--app/assets/stylesheets/pages/snippets.scss34
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss96
-rw-r--r--app/assets/stylesheets/pages/tree.scss32
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss11
-rw-r--r--app/assets/stylesheets/pages/wiki.scss5
-rw-r--r--app/assets/stylesheets/pages/xterm.scss52
-rw-r--r--app/controllers/abuse_reports_controller.rb12
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb6
-rw-r--r--app/controllers/admin/appearances_controller.rb57
-rw-r--r--app/controllers/admin/application_settings_controller.rb8
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb37
-rw-r--r--app/controllers/admin/builds_controller.rb6
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/spam_logs_controller.rb17
-rw-r--r--app/controllers/admin/users_controller.rb10
-rw-r--r--app/controllers/application_controller.rb53
-rw-r--r--app/controllers/ci/application_controller.rb47
-rw-r--r--app/controllers/ci/lints_controller.rb6
-rw-r--r--app/controllers/ci/projects_controller.rb13
-rw-r--r--app/controllers/concerns/continue_params.rb13
-rw-r--r--app/controllers/concerns/creates_commit.rb53
-rw-r--r--app/controllers/concerns/filter_projects.rb15
-rw-r--r--app/controllers/concerns/issues_action.rb4
-rw-r--r--app/controllers/concerns/merge_requests_action.rb4
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb17
-rw-r--r--app/controllers/dashboard/projects_controller.rb25
-rw-r--r--app/controllers/dashboard/todos_controller.rb44
-rw-r--r--app/controllers/dashboard_controller.rb6
-rw-r--r--app/controllers/emojis_controller.rb6
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb45
-rw-r--r--app/controllers/groups_controller.rb44
-rw-r--r--app/controllers/oauth/applications_controller.rb24
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb74
-rw-r--r--app/controllers/passwords_controller.rb8
-rw-r--r--app/controllers/profiles/keys_controller.rb8
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb12
-rw-r--r--app/controllers/profiles_controller.rb7
-rw-r--r--app/controllers/projects/application_controller.rb5
-rw-r--r--app/controllers/projects/artifacts_controller.rb46
-rw-r--r--app/controllers/projects/avatars_controller.rb17
-rw-r--r--app/controllers/projects/badges_controller.rb13
-rw-r--r--app/controllers/projects/blame_controller.rb24
-rw-r--r--app/controllers/projects/blob_controller.rb13
-rw-r--r--app/controllers/projects/branches_controller.rb24
-rw-r--r--app/controllers/projects/builds_controller.rb59
-rw-r--r--app/controllers/projects/commit_controller.rb66
-rw-r--r--app/controllers/projects/commits_controller.rb13
-rw-r--r--app/controllers/projects/compare_controller.rb38
-rw-r--r--app/controllers/projects/find_file_controller.rb26
-rw-r--r--app/controllers/projects/forks_controller.rb40
-rw-r--r--app/controllers/projects/group_links_controller.rb23
-rw-r--r--app/controllers/projects/imports_controller.rb34
-rw-r--r--app/controllers/projects/issues_controller.rb25
-rw-r--r--app/controllers/projects/labels_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb31
-rw-r--r--app/controllers/projects/milestones_controller.rb14
-rw-r--r--app/controllers/projects/notes_controller.rb24
-rw-r--r--app/controllers/projects/project_members_controller.rb1
-rw-r--r--app/controllers/projects/raw_controller.rb28
-rw-r--r--app/controllers/projects/refs_controller.rb8
-rw-r--r--app/controllers/projects/repositories_controller.rb4
-rw-r--r--app/controllers/projects/runner_projects_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb7
-rw-r--r--app/controllers/projects/triggers_controller.rb2
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb40
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/sent_notifications_controller.rb25
-rw-r--r--app/controllers/sessions_controller.rb20
-rw-r--r--app/controllers/uploads_controller.rb5
-rw-r--r--app/controllers/users_controller.rb69
-rw-r--r--app/finders/groups_finder.rb44
-rw-r--r--app/finders/issuable_finder.rb38
-rw-r--r--app/finders/issues_finder.rb6
-rw-r--r--app/finders/joined_groups_finder.rb49
-rw-r--r--app/finders/projects_finder.rb23
-rw-r--r--app/finders/snippets_finder.rb6
-rw-r--r--app/finders/todos_finder.rb129
-rw-r--r--app/helpers/appearances_helper.rb28
-rw-r--r--app/helpers/application_helper.rb53
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb6
-rw-r--r--app/helpers/blob_helper.rb86
-rw-r--r--app/helpers/broadcast_messages_helper.rb34
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb14
-rw-r--r--app/helpers/commits_helper.rb46
-rw-r--r--app/helpers/diff_helper.rb112
-rw-r--r--app/helpers/dropdowns_helper.rb100
-rw-r--r--app/helpers/events_helper.rb20
-rw-r--r--app/helpers/explore_helper.rb9
-rw-r--r--app/helpers/gitlab_markdown_helper.rb19
-rw-r--r--app/helpers/icons_helper.rb13
-rw-r--r--app/helpers/issuables_helper.rb58
-rw-r--r--app/helpers/issues_helper.rb23
-rw-r--r--app/helpers/labels_helper.rb42
-rw-r--r--app/helpers/milestones_helper.rb39
-rw-r--r--app/helpers/nav_helper.rb14
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/helpers/page_layout_helper.rb25
-rw-r--r--app/helpers/projects_helper.rb43
-rw-r--r--app/helpers/search_helper.rb6
-rw-r--r--app/helpers/snippets_helper.rb85
-rw-r--r--app/helpers/sorting_helper.rb36
-rw-r--r--app/helpers/todos_helper.rb87
-rw-r--r--app/helpers/tree_helper.rb3
-rw-r--r--app/mailers/abuse_report_mailer.rb10
-rw-r--r--app/mailers/email_rejection_mailer.rb2
-rw-r--r--app/mailers/emails/builds.rb14
-rw-r--r--app/mailers/emails/issues.rb58
-rw-r--r--app/mailers/emails/merge_requests.rb88
-rw-r--r--app/mailers/emails/notes.rb44
-rw-r--r--app/mailers/emails/profile.rb5
-rw-r--r--app/mailers/emails/projects.rb6
-rw-r--r--app/mailers/notify.rb20
-rw-r--r--app/models/ability.rb140
-rw-r--r--app/models/abuse_report.rb13
-rw-r--r--app/models/appearance.rb9
-rw-r--r--app/models/application_setting.rb38
-rw-r--r--app/models/blob.rb37
-rw-r--r--app/models/broadcast_message.rb20
-rw-r--r--app/models/ci/build.rb110
-rw-r--r--app/models/ci/commit.rb40
-rw-r--r--app/models/ci/runner.rb26
-rw-r--r--app/models/ci/runner_project.rb11
-rw-r--r--app/models/ci/trigger.rb17
-rw-r--r--app/models/ci/variable.rb9
-rw-r--r--app/models/commit.rb60
-rw-r--r--app/models/commit_range.rb4
-rw-r--r--app/models/commit_status.rb87
-rw-r--r--app/models/concerns/issuable.rb82
-rw-r--r--app/models/concerns/mentionable.rb7
-rw-r--r--app/models/concerns/milestoneish.rb29
-rw-r--r--app/models/concerns/sortable.rb3
-rw-r--r--app/models/concerns/subscribable.rb44
-rw-r--r--app/models/diff_line.rb3
-rw-r--r--app/models/email.rb2
-rw-r--r--app/models/event.rb18
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/generic_commit_status.rb1
-rw-r--r--app/models/global_label.rb7
-rw-r--r--app/models/global_milestone.rb59
-rw-r--r--app/models/group.rb25
-rw-r--r--app/models/hooks/project_hook.rb5
-rw-r--r--app/models/hooks/service_hook.rb5
-rw-r--r--app/models/hooks/system_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb11
-rw-r--r--app/models/identity.rb4
-rw-r--r--app/models/issue.rb45
-rw-r--r--app/models/key.rb3
-rw-r--r--app/models/label.rb70
-rw-r--r--app/models/member.rb8
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/merge_request.rb167
-rw-r--r--app/models/merge_request_diff.rb111
-rw-r--r--app/models/milestone.rb68
-rw-r--r--app/models/namespace.rb13
-rw-r--r--app/models/note.rb102
-rw-r--r--app/models/personal_snippet.rb1
-rw-r--r--app/models/project.rb157
-rw-r--r--app/models/project_group_link.rb36
-rw-r--r--app/models/project_services/asana_service.rb84
-rw-r--r--app/models/project_services/assembla_service.rb1
-rw-r--r--app/models/project_services/bamboo_service.rb1
-rw-r--r--app/models/project_services/buildkite_service.rb1
-rw-r--r--app/models/project_services/builds_email_service.rb7
-rw-r--r--app/models/project_services/campfire_service.rb1
-rw-r--r--app/models/project_services/ci_service.rb11
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb1
-rw-r--r--app/models/project_services/drone_ci_service.rb1
-rw-r--r--app/models/project_services/emails_on_push_service.rb1
-rw-r--r--app/models/project_services/external_wiki_service.rb1
-rw-r--r--app/models/project_services/flowdock_service.rb1
-rw-r--r--app/models/project_services/gemnasium_service.rb1
-rw-r--r--app/models/project_services/gitlab_ci_service.rb1
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb5
-rw-r--r--app/models/project_services/hipchat_service.rb9
-rw-r--r--app/models/project_services/irker_service.rb8
-rw-r--r--app/models/project_services/issue_tracker_service.rb7
-rw-r--r--app/models/project_services/jira_service.rb17
-rw-r--r--app/models/project_services/pivotaltracker_service.rb1
-rw-r--r--app/models/project_services/pushover_service.rb3
-rw-r--r--app/models/project_services/redmine_service.rb1
-rw-r--r--app/models/project_services/slack_service.rb1
-rw-r--r--app/models/project_services/teamcity_service.rb1
-rw-r--r--app/models/project_snippet.rb3
-rw-r--r--app/models/project_team.rb54
-rw-r--r--app/models/project_wiki.rb15
-rw-r--r--app/models/repository.rb285
-rw-r--r--app/models/sent_notification.rb12
-rw-r--r--app/models/service.rb12
-rw-r--r--app/models/snippet.rb31
-rw-r--r--app/models/spam_log.rb10
-rw-r--r--app/models/spam_report.rb5
-rw-r--r--app/models/subscription.rb2
-rw-r--r--app/models/todo.rb53
-rw-r--r--app/models/tree.rb22
-rw-r--r--app/models/user.rb198
-rw-r--r--app/models/wiki_page.rb6
-rw-r--r--app/services/archive_repository_service.rb23
-rw-r--r--app/services/base_service.rb4
-rw-r--r--app/services/ci/create_builds_service.rb1
-rw-r--r--app/services/ci/image_for_build_service.rb21
-rw-r--r--app/services/commits/revert_service.rb59
-rw-r--r--app/services/compare_service.rb12
-rw-r--r--app/services/create_branch_service.rb7
-rw-r--r--app/services/create_commit_builds_service.rb1
-rw-r--r--app/services/create_spam_log_service.rb13
-rw-r--r--app/services/create_tag_service.rb1
-rw-r--r--app/services/delete_branch_service.rb7
-rw-r--r--app/services/delete_user_service.rb24
-rw-r--r--app/services/destroy_group_service.rb6
-rw-r--r--app/services/git_push_service.rb142
-rw-r--r--app/services/git_tag_push_service.rb2
-rw-r--r--app/services/issuable_base_service.rb26
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/create_service.rb1
-rw-r--r--app/services/issues/update_service.rb17
-rw-r--r--app/services/merge_requests/build_service.rb37
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/create_service.rb3
-rw-r--r--app/services/merge_requests/merge_service.rb5
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb27
-rw-r--r--app/services/merge_requests/post_merge_service.rb4
-rw-r--r--app/services/merge_requests/update_service.rb21
-rw-r--r--app/services/notes/create_service.rb21
-rw-r--r--app/services/notes/post_process_service.rb28
-rw-r--r--app/services/notes/update_service.rb4
-rw-r--r--app/services/notification_service.rb90
-rw-r--r--app/services/projects/autocomplete_service.rb6
-rw-r--r--app/services/projects/destroy_service.rb18
-rw-r--r--app/services/projects/download_service.rb8
-rw-r--r--app/services/projects/housekeeping_service.rb47
-rw-r--r--app/services/projects/import_service.rb67
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/projects/upload_service.rb8
-rw-r--r--app/services/repair_ldap_blocked_user_service.rb17
-rw-r--r--app/services/search/global_service.rb3
-rw-r--r--app/services/search/project_service.rb3
-rw-r--r--app/services/search/snippet_service.rb5
-rw-r--r--app/services/system_hooks_service.rb17
-rw-r--r--app/services/system_note_service.rb37
-rw-r--r--app/services/todo_service.rb170
-rw-r--r--app/uploaders/artifact_uploader.rb4
-rw-r--r--app/uploaders/file_uploader.rb15
-rw-r--r--app/validators/email_validator.rb15
-rw-r--r--app/validators/namespace_validator.rb1
-rw-r--r--app/validators/url_validator.rb3
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml2
-rw-r--r--app/views/abuse_reports/new.html.haml4
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml23
-rw-r--r--app/views/admin/abuse_reports/index.html.haml3
-rw-r--r--app/views/admin/appearances/_form.html.haml58
-rw-r--r--app/views/admin/appearances/preview.html.haml29
-rw-r--r--app/views/admin/appearances/show.html.haml7
-rw-r--r--app/views/admin/application_settings/_form.html.haml77
-rw-r--r--app/views/admin/applications/_form.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml40
-rw-r--r--app/views/admin/broadcast_messages/edit.html.haml3
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml81
-rw-r--r--app/views/admin/broadcast_messages/preview.js.haml1
-rw-r--r--app/views/admin/builds/_build.html.haml25
-rw-r--r--app/views/admin/builds/index.html.haml27
-rw-r--r--app/views/admin/dashboard/index.html.haml27
-rw-r--r--app/views/admin/deploy_keys/index.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml3
-rw-r--r--app/views/admin/groups/index.html.haml4
-rw-r--r--app/views/admin/groups/show.html.haml18
-rw-r--r--app/views/admin/hooks/index.html.haml3
-rw-r--r--app/views/admin/labels/_form.html.haml4
-rw-r--r--app/views/admin/labels/_label.html.haml10
-rw-r--r--app/views/admin/labels/index.html.haml2
-rw-r--r--app/views/admin/logs/show.html.haml5
-rw-r--r--app/views/admin/projects/index.html.haml6
-rw-r--r--app/views/admin/projects/show.html.haml4
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml32
-rw-r--r--app/views/admin/spam_logs/index.html.haml21
-rw-r--r--app/views/admin/users/_form.html.haml8
-rw-r--r--app/views/admin/users/_head.html.haml7
-rw-r--r--app/views/admin/users/_profile.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml196
-rw-r--r--app/views/admin/users/keys.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml28
-rw-r--r--app/views/ci/commits/_commit.html.haml32
-rw-r--r--app/views/ci/lints/show.html.haml6
-rw-r--r--app/views/dashboard/_activities.html.haml4
-rw-r--r--app/views/dashboard/_activity_head.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml20
-rw-r--r--app/views/dashboard/_projects_head.html.haml14
-rw-r--r--app/views/dashboard/_snippets_head.html.haml2
-rw-r--r--app/views/dashboard/groups/index.html.haml9
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/issues.html.haml19
-rw-r--r--app/views/dashboard/merge_requests.html.haml10
-rw-r--r--app/views/dashboard/milestones/_issue.html.haml10
-rw-r--r--app/views/dashboard/milestones/_issues.html.haml6
-rw-r--r--app/views/dashboard/milestones/_merge_request.html.haml10
-rw-r--r--app/views/dashboard/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml31
-rw-r--r--app/views/dashboard/milestones/index.html.haml9
-rw-r--r--app/views/dashboard/milestones/show.html.haml106
-rw-r--r--app/views/dashboard/projects/_projects.html.haml4
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml8
-rw-r--r--app/views/dashboard/projects/index.atom.builder2
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/snippets/index.html.haml48
-rw-r--r--app/views/dashboard/todos/_todo.html.haml26
-rw-r--r--app/views/dashboard/todos/index.html.haml66
-rw-r--r--app/views/devise/sessions/new.html.haml6
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml8
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml31
-rw-r--r--app/views/doorkeeper/applications/index.html.haml98
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml11
-rw-r--r--app/views/emojis/index.html.haml11
-rw-r--r--app/views/events/_commit.html.haml2
-rw-r--r--app/views/events/_event.html.haml6
-rw-r--r--app/views/events/_event_last_push.html.haml4
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_dropdown.html.haml27
-rw-r--r--app/views/explore/projects/_filter.html.haml81
-rw-r--r--app/views/explore/projects/_nav.html.haml10
-rw-r--r--app/views/explore/projects/_projects.html.haml7
-rw-r--r--app/views/explore/projects/index.html.haml9
-rw-r--r--app/views/explore/projects/starred.html.haml11
-rw-r--r--app/views/explore/projects/trending.html.haml10
-rw-r--r--app/views/groups/_activities.html.haml12
-rw-r--r--app/views/groups/_projects.html.haml12
-rw-r--r--app/views/groups/_shared_projects.html.haml1
-rw-r--r--app/views/groups/activity.html.haml9
-rw-r--r--app/views/groups/edit.html.haml11
-rw-r--r--app/views/groups/group_members/_group_member.html.haml3
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml5
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/issues.html.haml16
-rw-r--r--app/views/groups/merge_requests.html.haml7
-rw-r--r--app/views/groups/milestones/_issue.html.haml10
-rw-r--r--app/views/groups/milestones/_issues.html.haml6
-rw-r--r--app/views/groups/milestones/_merge_request.html.haml10
-rw-r--r--app/views/groups/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/groups/milestones/_milestone.html.haml34
-rw-r--r--app/views/groups/milestones/index.html.haml16
-rw-r--r--app/views/groups/milestones/new.html.haml6
-rw-r--r--app/views/groups/milestones/show.html.haml114
-rw-r--r--app/views/groups/projects.html.haml6
-rw-r--r--app/views/groups/show.atom.builder2
-rw-r--r--app/views/groups/show.html.haml63
-rw-r--r--app/views/help/_shortcuts.html.haml42
-rw-r--r--app/views/help/ui.html.haml474
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml8
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml8
-rw-r--r--app/views/layouts/_broadcast.html.haml5
-rw-r--r--app/views/layouts/_head.html.haml8
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml8
-rw-r--r--app/views/layouts/_page.html.haml7
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/application.html.haml6
-rw-r--r--app/views/layouts/ci/_page.html.haml3
-rw-r--r--app/views/layouts/group.html.haml7
-rw-r--r--app/views/layouts/header/_default.html.haml48
-rw-r--r--app/views/layouts/header/_public.html.haml10
-rw-r--r--app/views/layouts/nav/_admin.html.haml25
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml20
-rw-r--r--app/views/layouts/nav/_group.html.haml11
-rw-r--r--app/views/layouts/nav/_profile.html.haml6
-rw-r--r--app/views/layouts/nav/_project.html.haml18
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml10
-rw-r--r--app/views/layouts/notify.html.haml13
-rw-r--r--app/views/layouts/project.html.haml7
-rw-r--r--app/views/notify/_note_message.html.haml3
-rw-r--r--app/views/notify/_reassigned_issuable_email.text.erb2
-rw-r--r--app/views/notify/_relabeled_issuable_email.html.haml3
-rw-r--r--app/views/notify/_relabeled_issuable_email.text.erb3
-rw-r--r--app/views/notify/build_fail_email.html.haml3
-rw-r--r--app/views/notify/build_success_email.html.haml2
-rw-r--r--app/views/notify/new_issue_email.html.haml3
-rw-r--r--app/views/notify/new_merge_request_email.html.haml3
-rw-r--r--app/views/notify/relabeled_issue_email.html.haml1
-rw-r--r--app/views/notify/relabeled_issue_email.text.erb1
-rw-r--r--app/views/notify/relabeled_merge_request_email.html.haml1
-rw-r--r--app/views/notify/relabeled_merge_request_email.text.erb1
-rw-r--r--app/views/notify/repository_push_email.html.haml4
-rw-r--r--app/views/notify/repository_push_email.text.haml2
-rw-r--r--app/views/profiles/_event_table.html.haml28
-rw-r--r--app/views/profiles/accounts/show.html.haml215
-rw-r--r--app/views/profiles/applications.html.haml70
-rw-r--r--app/views/profiles/audit_log.html.haml13
-rw-r--r--app/views/profiles/emails/index.html.haml93
-rw-r--r--app/views/profiles/keys/_form.html.haml14
-rw-r--r--app/views/profiles/keys/_key.html.haml25
-rw-r--r--app/views/profiles/keys/_key_details.html.haml4
-rw-r--r--app/views/profiles/keys/_key_table.html.haml20
-rw-r--r--app/views/profiles/keys/index.html.haml29
-rw-r--r--app/views/profiles/keys/new.html.haml17
-rw-r--r--app/views/profiles/notifications/_settings.html.haml4
-rw-r--r--app/views/profiles/notifications/show.html.haml123
-rw-r--r--app/views/profiles/passwords/edit.html.haml51
-rw-r--r--app/views/profiles/preferences/show.html.haml99
-rw-r--r--app/views/profiles/show.html.haml161
-rw-r--r--app/views/profiles/two_factor_auths/new.html.haml78
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_builds_settings.html.haml60
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_find_file_link.html.haml3
-rw-r--r--app/views/projects/_home_panel.html.haml49
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_zen.html.haml11
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml8
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml9
-rw-r--r--app/views/projects/artifacts/browse.html.haml22
-rw-r--r--app/views/projects/blame/show.html.haml26
-rw-r--r--app/views/projects/blob/_blob.html.haml11
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/_image.html.haml9
-rw-r--r--app/views/projects/blob/_new_dir.html.haml2
-rw-r--r--app/views/projects/blob/_remove.html.haml2
-rw-r--r--app/views/projects/blob/_text.html.haml9
-rw-r--r--app/views/projects/blob/_upload.html.haml2
-rw-r--r--app/views/projects/blob/diff.html.haml5
-rw-r--r--app/views/projects/blob/edit.html.haml4
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml14
-rw-r--r--app/views/projects/branches/_branch.html.haml17
-rw-r--r--app/views/projects/branches/destroy.js.haml2
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/builds/index.html.haml37
-rw-r--r--app/views/projects/builds/show.html.haml98
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml11
-rw-r--r--app/views/projects/ci/builds/_build.html.haml76
-rw-r--r--app/views/projects/commit/_builds.html.haml9
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/_revert.html.haml31
-rw-r--r--app/views/projects/commit/builds.html.haml3
-rw-r--r--app/views/projects/commit/show.html.haml13
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml79
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_commit_list.html.haml11
-rw-r--r--app/views/projects/commits/_commits.html.haml10
-rw-r--r--app/views/projects/commits/_head.html.haml6
-rw-r--r--app/views/projects/commits/show.atom.builder4
-rw-r--r--app/views/projects/commits/show.html.haml33
-rw-r--r--app/views/projects/compare/_form.html.haml5
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml15
-rw-r--r--app/views/projects/diffs/_file.html.haml64
-rw-r--r--app/views/projects/diffs/_image.html.haml16
-rw-r--r--app/views/projects/diffs/_match_line.html.haml2
-rw-r--r--app/views/projects/diffs/_match_line_parallel.html.haml8
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml52
-rw-r--r--app/views/projects/diffs/_text_file.html.haml24
-rw-r--r--app/views/projects/diffs/_warning.html.haml13
-rw-r--r--app/views/projects/edit.html.haml74
-rw-r--r--app/views/projects/empty.html.haml73
-rw-r--r--app/views/projects/find_file/show.html.haml27
-rw-r--r--app/views/projects/forks/_projects.html.haml2
-rw-r--r--app/views/projects/forks/index.html.haml48
-rw-r--r--app/views/projects/forks/new.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml58
-rw-r--r--app/views/projects/go_import.html.haml5
-rw-r--r--app/views/projects/graphs/_head.html.haml2
-rw-r--r--app/views/projects/group_links/index.html.haml41
-rw-r--r--app/views/projects/hooks/index.html.haml14
-rw-r--r--app/views/projects/issues/_closed_by_box.html.haml6
-rw-r--r--app/views/projects/issues/_discussion.html.haml6
-rw-r--r--app/views/projects/issues/_form.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml14
-rw-r--r--app/views/projects/issues/_issues.html.haml5
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml8
-rw-r--r--app/views/projects/issues/_new_branch.html.haml5
-rw-r--r--app/views/projects/issues/_related_branches.html.haml15
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/index.html.haml21
-rw-r--r--app/views/projects/issues/show.html.haml78
-rw-r--r--app/views/projects/issues/update.js.haml6
-rw-r--r--app/views/projects/labels/_form.html.haml8
-rw-r--r--app/views/projects/labels/_label.html.haml17
-rw-r--r--app/views/projects/labels/index.html.haml13
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml17
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml5
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml29
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml12
-rw-r--r--app/views/projects/merge_requests/_show.html.haml21
-rw-r--r--app/views/projects/merge_requests/index.html.haml20
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml36
-rw-r--r--app/views/projects/merge_requests/update.js.haml6
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml14
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml11
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/milestones/_form.html.haml6
-rw-r--r--app/views/projects/milestones/_issue.html.haml9
-rw-r--r--app/views/projects/milestones/_issues.html.haml6
-rw-r--r--app/views/projects/milestones/_merge_request.html.haml8
-rw-r--r--app/views/projects/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/projects/milestones/_milestone.html.haml35
-rw-r--r--app/views/projects/milestones/index.html.haml15
-rw-r--r--app/views/projects/milestones/show.html.haml101
-rw-r--r--app/views/projects/notes/_diff_notes_with_reply.html.haml2
-rw-r--r--app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml5
-rw-r--r--app/views/projects/notes/_edit_form.html.haml8
-rw-r--r--app/views/projects/notes/_form.html.haml9
-rw-r--r--app/views/projects/notes/_note.html.haml12
-rw-r--r--app/views/projects/notes/_notes.html.haml4
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml10
-rw-r--r--app/views/projects/notes/discussions/_commit.html.haml3
-rw-r--r--app/views/projects/notes/discussions/_diff.html.haml14
-rw-r--r--app/views/projects/project_members/_group_members.html.haml2
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml21
-rw-r--r--app/views/projects/project_members/_team.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml8
-rw-r--r--app/views/projects/refs/logs_tree.js.haml7
-rw-r--r--app/views/projects/releases/edit.html.haml4
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml2
-rw-r--r--app/views/projects/runners/index.html.haml3
-rw-r--r--app/views/projects/show.atom.builder2
-rw-r--r--app/views/projects/show.html.haml34
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/destroy.js.haml3
-rw-r--r--app/views/projects/tags/new.html.haml4
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml6
-rw-r--r--app/views/projects/tree/show.html.haml10
-rw-r--r--app/views/projects/variables/show.html.haml4
-rw-r--r--app/views/projects/wikis/_form.html.haml4
-rw-r--r--app/views/projects/wikis/_main_links.html.haml23
-rw-r--r--app/views/projects/wikis/_nav.html.haml20
-rw-r--r--app/views/projects/wikis/_new.html.haml16
-rw-r--r--app/views/projects/wikis/edit.html.haml22
-rw-r--r--app/views/projects/wikis/git_access.html.haml12
-rw-r--r--app/views/projects/wikis/history.html.haml13
-rw-r--r--app/views/projects/wikis/pages.html.haml13
-rw-r--r--app/views/projects/wikis/show.html.haml11
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/_filter.html.haml46
-rw-r--r--app/views/search/_form.html.haml2
-rw-r--r--app/views/search/_results.html.haml4
-rw-r--r--app/views/search/results/_issue.html.haml1
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml74
-rw-r--r--app/views/search/results/_wiki_blob.html.haml4
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/shared/_clone_panel.html.haml4
-rw-r--r--app/views/shared/_commit_message_container.html.haml2
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_file_highlight.html.haml9
-rw-r--r--app/views/shared/_import_form.html.haml2
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml4
-rw-r--r--app/views/shared/_logo.svg28
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_milestones_filter.html.haml21
-rw-r--r--app/views/shared/_new_project_item_select.html.haml8
-rw-r--r--app/views/shared/_no_ssh.html.haml2
-rw-r--r--app/views/shared/_project_limit.html.haml2
-rw-r--r--app/views/shared/_promo.html.haml6
-rw-r--r--app/views/shared/_service_settings.html.haml4
-rw-r--r--app/views/shared/_sort_dropdown.html.haml6
-rw-r--r--app/views/shared/groups/_group.html.haml26
-rw-r--r--app/views/shared/groups/_list.html.haml6
-rw-r--r--app/views/shared/issuable/_filter.html.haml120
-rw-r--r--app/views/shared/issuable/_form.html.haml13
-rw-r--r--app/views/shared/issuable/_nav.html.haml25
-rw-r--r--app/views/shared/issuable/_participants.html.haml9
-rw-r--r--app/views/shared/issuable/_search_form.html.haml17
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml185
-rw-r--r--app/views/shared/milestones/_issuable.html.haml27
-rw-r--r--app/views/shared/milestones/_issuables.html.haml16
-rw-r--r--app/views/shared/milestones/_issues_tab.html.haml10
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml18
-rw-r--r--app/views/shared/milestones/_merge_requests_tab.haml12
-rw-r--r--app/views/shared/milestones/_milestone.html.haml45
-rw-r--r--app/views/shared/milestones/_participants_tab.html.haml8
-rw-r--r--app/views/shared/milestones/_summary.html.haml28
-rw-r--r--app/views/shared/milestones/_tabs.html.haml30
-rw-r--r--app/views/shared/milestones/_top.html.haml58
-rw-r--r--app/views/shared/projects/_dropdown.html.haml22
-rw-r--r--app/views/shared/projects/_list.html.haml29
-rw-r--r--app/views/shared/projects/_project.html.haml44
-rw-r--r--app/views/shared/snippets/_blob.html.haml5
-rw-r--r--app/views/shared/snippets/_snippet.html.haml7
-rw-r--r--app/views/sherlock/queries/show.html.haml2
-rw-r--r--app/views/sherlock/transactions/_queries.html.haml2
-rw-r--r--app/views/sherlock/transactions/show.html.haml2
-rw-r--r--app/views/snippets/_snippets.html.haml2
-rw-r--r--app/views/users/show.atom.builder2
-rw-r--r--app/views/users/show.html.haml211
-rw-r--r--app/views/votes/_votes_block.html.haml46
-rw-r--r--app/workers/delete_user_worker.rb10
-rw-r--r--app/workers/irker_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb12
-rw-r--r--app/workers/post_receive.rb46
-rw-r--r--app/workers/project_destroy_worker.rb17
-rw-r--r--app/workers/repository_fork_worker.rb1
-rw-r--r--app/workers/repository_import_worker.rb47
-rwxr-xr-xbin/background_jobs9
-rwxr-xr-xbin/web11
-rw-r--r--config.ru5
-rw-r--r--config/application.rb46
-rw-r--r--config/database.yml.env12
-rw-r--r--config/environments/development.rb5
-rw-r--r--config/environments/test.rb2
-rw-r--r--config/gitlab.yml.example30
-rw-r--r--config/initializers/1_settings.rb32
-rw-r--r--config/initializers/2_app.rb4
-rw-r--r--config/initializers/date_time_formats.rb9
-rw-r--r--config/initializers/devise.rb10
-rw-r--r--config/initializers/go_get.rb1
-rw-r--r--config/initializers/gollum.rb13
-rw-r--r--config/initializers/haml.rb6
-rw-r--r--config/initializers/metrics.rb22
-rw-r--r--config/initializers/monkey_patch.rb48
-rw-r--r--config/initializers/mysql_ignore_postgresql_options.rb49
-rw-r--r--config/initializers/postgresql_opclasses_support.rb188
-rw-r--r--config/initializers/relative_url.rb.sample10
-rw-r--r--config/initializers/sentry.rb20
-rw-r--r--config/initializers/session_store.rb5
-rw-r--r--config/initializers/sidekiq.rb17
-rw-r--r--config/initializers/smtp_settings.rb.sample2
-rw-r--r--config/locales/en.yml4
-rw-r--r--config/mail_room.yml80
-rw-r--r--config/routes.rb102
-rw-r--r--config/sidekiq.yml.example2
-rw-r--r--config/unicorn.rb.example9
-rw-r--r--db/fixtures/development/14_builds.rb90
-rw-r--r--db/fixtures/production/001_admin.rb53
-rw-r--r--db/migrate/20130711063759_create_project_group_links.rb10
-rw-r--r--db/migrate/20130820102832_add_access_to_project_group_link.rb5
-rw-r--r--db/migrate/20150930110012_add_group_share_lock.rb5
-rw-r--r--db/migrate/20151201203948_raise_hook_url_limit.rb5
-rw-r--r--db/migrate/20151228111122_remove_public_from_namespace.rb6
-rw-r--r--db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb5
-rw-r--r--db/migrate/20151231152326_add_akismet_to_application_settings.rb8
-rw-r--r--db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb5
-rw-r--r--db/migrate/20160106162223_add_index_milestones_title.rb5
-rw-r--r--db/migrate/20160106164438_remove_influxdb_credentials.rb6
-rw-r--r--db/migrate/20160109054846_create_spam_logs.rb16
-rw-r--r--db/migrate/20160113111034_add_metrics_sample_interval.rb6
-rw-r--r--db/migrate/20160118155830_add_sentry_to_application_settings.rb8
-rw-r--r--db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb6
-rw-r--r--db/migrate/20160119111158_add_services_category.rb39
-rw-r--r--db/migrate/20160119112418_add_services_default.rb20
-rw-r--r--db/migrate/20160119145451_add_ldap_email_to_users.rb30
-rw-r--r--db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb5
-rw-r--r--db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb5
-rw-r--r--db/migrate/20160122185421_add_pending_delete_to_project.rb5
-rw-r--r--db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb6
-rw-r--r--db/migrate/20160128233227_change_lfs_objects_size_column.rb5
-rw-r--r--db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb80
-rw-r--r--db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb5
-rw-r--r--db/migrate/20160202091601_add_erasable_to_ci_build.rb6
-rw-r--r--db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb5
-rw-r--r--db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb5
-rw-r--r--db/migrate/20160209130428_add_index_to_snippet.rb5
-rw-r--r--db/migrate/20160212123307_create_tasks.rb14
-rw-r--r--db/migrate/20160217100506_add_description_to_label.rb5
-rw-r--r--db/migrate/20160217174422_add_note_to_tasks.rb5
-rw-r--r--db/migrate/20160220123949_rename_tasks_to_todos.rb5
-rw-r--r--db/migrate/20160222153918_create_appearances_ce.rb14
-rw-r--r--db/migrate/20160223192159_add_confidential_to_issues.rb6
-rw-r--r--db/migrate/20160226114608_add_trigram_indexes_for_searching.rb53
-rw-r--r--db/migrate/20160229193553_add_main_language_to_repository.rb5
-rw-r--r--db/migrate/20160305220806_remove_expires_at_from_snippets.rb5
-rw-r--r--db/migrate/20160307221555_disallow_blank_line_code_on_note.rb9
-rw-r--r--db/migrate/20160309140734_fix_todos.rb16
-rw-r--r--db/migrate/20160310185910_add_external_flag_to_users.rb5
-rw-r--r--db/migrate/20160314143402_projects_add_pushes_since_gc.rb5
-rw-r--r--db/migrate/20160316123110_ci_runners_token_index.rb13
-rw-r--r--db/migrate/limits_to_mysql.rb1
-rw-r--r--db/schema.rb150
-rw-r--r--doc/README.md48
-rw-r--r--doc/administration/enviroment_variables.md48
-rw-r--r--doc/administration/environment_variables.md61
-rw-r--r--doc/administration/housekeeping.md22
-rw-r--r--doc/administration/img/housekeeping_settings.pngbin0 -> 23856 bytes
-rw-r--r--doc/administration/restart_gitlab.md145
-rw-r--r--doc/api/README.md301
-rw-r--r--doc/api/branches.md108
-rw-r--r--doc/api/build_triggers.md108
-rw-r--r--doc/api/build_variables.md128
-rw-r--r--doc/api/builds.md421
-rw-r--r--doc/api/commits.md264
-rw-r--r--doc/api/deploy_key_multiple_projects.md18
-rw-r--r--doc/api/deploy_keys.md74
-rw-r--r--doc/api/groups.md1
-rw-r--r--doc/api/issues.md404
-rw-r--r--doc/api/labels.md152
-rw-r--r--doc/api/merge_requests.md240
-rw-r--r--doc/api/namespaces.md40
-rw-r--r--doc/api/projects.md69
-rw-r--r--doc/api/runners.md322
-rw-r--r--doc/api/session.md42
-rw-r--r--doc/api/settings.md98
-rw-r--r--doc/api/system_hooks.md118
-rw-r--r--doc/api/tags.md20
-rw-r--r--doc/api/users.md17
-rw-r--r--doc/ci/README.md51
-rw-r--r--doc/ci/api/README.md88
-rw-r--r--doc/ci/api/builds.md72
-rw-r--r--doc/ci/api/commits.md101
-rw-r--r--doc/ci/api/projects.md149
-rw-r--r--doc/ci/api/runners.md73
-rw-r--r--doc/ci/build_artifacts/README.md176
-rw-r--r--doc/ci/build_artifacts/img/build_artifacts_browser.pngbin0 -> 89132 bytes
-rw-r--r--doc/ci/build_artifacts/img/build_artifacts_browser_button.pngbin0 -> 11614 bytes
-rw-r--r--doc/ci/deployment/README.md2
-rw-r--r--doc/ci/docker/using_docker_images.md4
-rw-r--r--doc/ci/enable_or_disable_ci.md70
-rw-r--r--doc/ci/examples/README.md18
-rw-r--r--doc/ci/examples/php.md (renamed from doc/ci/languages/php.md)6
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md8
-rw-r--r--doc/ci/img/features_settings.pngbin0 -> 18691 bytes
-rw-r--r--doc/ci/languages/README.md7
-rw-r--r--doc/ci/quick_start/README.md81
-rw-r--r--doc/ci/runners/README.md5
-rw-r--r--doc/ci/services/README.md12
-rw-r--r--doc/ci/variables/README.md13
-rw-r--r--doc/ci/yaml/README.md446
-rw-r--r--doc/customization/branded_login_page.md19
-rw-r--r--doc/customization/branded_login_page/appearance.pngbin0 -> 365120 bytes
-rw-r--r--doc/customization/branded_login_page/custom_sign_in.pngbin0 -> 314111 bytes
-rw-r--r--doc/customization/branded_login_page/default_login_page.pngbin0 -> 292731 bytes
-rw-r--r--doc/customization/issue_closing.md2
-rw-r--r--doc/customization/welcome_message.md8
-rw-r--r--doc/development/README.md11
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/benchmarking.md69
-rw-r--r--doc/development/ci_setup.md2
-rw-r--r--doc/development/doc_styleguide.md270
-rw-r--r--doc/development/gotchas.md103
-rw-r--r--doc/development/migration_style_guide.md8
-rw-r--r--doc/development/scss_styleguide.md194
-rw-r--r--doc/development/sql.md219
-rw-r--r--doc/gitlab-basics/basicsimages/compare_branches.png (renamed from doc/gitlab-basics/basicsimages/compare_braches.png)bin1624 -> 1624 bytes
-rw-r--r--doc/hooks/custom_hooks.md2
-rw-r--r--doc/incoming_email/README.md3
-rw-r--r--doc/incoming_email/postfix.md15
-rw-r--r--doc/install/database_mysql.md26
-rw-r--r--doc/install/installation.md112
-rw-r--r--doc/install/redis.md60
-rw-r--r--doc/install/relative_url.md136
-rw-r--r--doc/install/requirements.md29
-rw-r--r--doc/integration/README.md65
-rw-r--r--doc/integration/akismet.md30
-rw-r--r--doc/integration/auth0.md89
-rw-r--r--doc/integration/azure.md83
-rw-r--r--doc/integration/external-issue-tracker.md48
-rw-r--r--doc/integration/facebook.md6
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/integration/gitlab.md2
-rw-r--r--doc/integration/gmail_action_buttons_for_gitlab.md2
-rw-r--r--doc/integration/google.md2
-rw-r--r--doc/integration/img/akismet_settings.pngbin0 -> 55837 bytes
-rw-r--r--doc/integration/img/facebook_api_keys.png (renamed from doc/integration/facebook_api_keys.png)bin125921 -> 125921 bytes
-rw-r--r--doc/integration/img/facebook_app_settings.png (renamed from doc/integration/facebook_app_settings.png)bin134387 -> 134387 bytes
-rw-r--r--doc/integration/img/facebook_website_url.png (renamed from doc/integration/facebook_website_url.png)bin42292 -> 42292 bytes
-rw-r--r--doc/integration/img/github_app.png (renamed from doc/integration/github_app.png)bin75297 -> 75297 bytes
-rw-r--r--doc/integration/img/gitlab_app.png (renamed from doc/integration/gitlab_app.png)bin55325 -> 55325 bytes
-rw-r--r--doc/integration/img/gmail_action_buttons_for_gitlab.png (renamed from doc/integration/gmail_actions_button.png)bin17321 -> 17321 bytes
-rw-r--r--doc/integration/img/google_app.png (renamed from doc/integration/google_app.png)bin52669 -> 52669 bytes
-rw-r--r--doc/integration/img/oauth_provider_admin_application.pngbin0 -> 40579 bytes
-rw-r--r--doc/integration/img/oauth_provider_application_form.pngbin0 -> 27974 bytes
-rw-r--r--doc/integration/img/oauth_provider_application_id_secret.pngbin0 -> 33901 bytes
-rw-r--r--doc/integration/img/oauth_provider_authorized_application.pngbin0 -> 32225 bytes
-rw-r--r--doc/integration/img/oauth_provider_user_wide_applications.pngbin0 -> 40632 bytes
-rw-r--r--doc/integration/img/twitter_app_api_keys.png (renamed from doc/integration/twitter_app_api_keys.png)bin72200 -> 72200 bytes
-rw-r--r--doc/integration/img/twitter_app_details.png (renamed from doc/integration/twitter_app_details.png)bin121621 -> 121621 bytes
-rw-r--r--doc/integration/jira-integration-points.pngbin67854 -> 0 bytes
-rw-r--r--doc/integration/jira.md114
-rw-r--r--doc/integration/jira_service_page.pngbin162449 -> 0 bytes
-rw-r--r--doc/integration/ldap.md27
-rw-r--r--doc/integration/oauth_provider.md89
-rw-r--r--doc/integration/oauth_provider/admin_application.pngbin55533 -> 0 bytes
-rw-r--r--doc/integration/oauth_provider/application_form.pngbin25075 -> 0 bytes
-rw-r--r--doc/integration/oauth_provider/authorized_application.pngbin17260 -> 0 bytes
-rw-r--r--doc/integration/oauth_provider/user_wide_applications.pngbin46238 -> 0 bytes
-rw-r--r--doc/integration/omniauth.md116
-rw-r--r--doc/integration/redmine_configuration.pngbin118752 -> 0 bytes
-rw-r--r--doc/integration/redmine_service_template.pngbin198077 -> 0 bytes
-rw-r--r--doc/integration/saml.md205
-rw-r--r--doc/integration/shibboleth.md4
-rw-r--r--doc/integration/slack.md12
-rw-r--r--doc/integration/twitter.md6
-rw-r--r--doc/legal/individual_contributor_license_agreement.md2
-rw-r--r--doc/markdown/img/logo.pngbin0 -> 11097 bytes
-rw-r--r--doc/markdown/markdown.md17
-rw-r--r--doc/monitoring/performance/gitlab_configuration.md39
-rw-r--r--doc/monitoring/performance/img/metrics_gitlab_configuration_settings.pngbin0 -> 45148 bytes
-rw-r--r--doc/monitoring/performance/influxdb_configuration.md192
-rw-r--r--doc/monitoring/performance/influxdb_schema.md87
-rw-r--r--doc/monitoring/performance/introduction.md64
-rw-r--r--doc/permissions/permissions.md34
-rw-r--r--doc/profile/preferences.md5
-rw-r--r--doc/project_services/builds_emails.md16
-rw-r--r--doc/project_services/img/builds_emails_service.pngbin0 -> 41222 bytes
-rw-r--r--doc/project_services/img/jira_add_gitlab_commit_message.pngbin0 -> 57136 bytes
-rw-r--r--doc/project_services/img/jira_add_user_to_group.pngbin0 -> 59251 bytes
-rw-r--r--doc/project_services/img/jira_create_new_group.pngbin0 -> 41294 bytes
-rw-r--r--doc/project_services/img/jira_create_new_group_name.pngbin0 -> 12535 bytes
-rw-r--r--doc/project_services/img/jira_create_new_user.pngbin0 -> 26532 bytes
-rw-r--r--doc/project_services/img/jira_group_access.pngbin0 -> 46028 bytes
-rw-r--r--doc/project_services/img/jira_issue_closed.pngbin0 -> 92601 bytes
-rw-r--r--doc/project_services/img/jira_issue_reference.png (renamed from doc/integration/jira_issue_reference.png)bin39942 -> 39942 bytes
-rw-r--r--doc/project_services/img/jira_issues_workflow.pngbin0 -> 105237 bytes
-rw-r--r--doc/project_services/img/jira_merge_request_close.pngbin0 -> 111150 bytes
-rw-r--r--doc/project_services/img/jira_project_name.png (renamed from doc/integration/jira_project_name.png)bin60598 -> 60598 bytes
-rw-r--r--doc/project_services/img/jira_reference_commit_message_in_jira_issue.pngbin0 -> 42452 bytes
-rw-r--r--doc/project_services/img/jira_service.png (renamed from doc/integration/jira_service.png)bin59082 -> 59082 bytes
-rw-r--r--doc/project_services/img/jira_service_close_issue.png (renamed from doc/integration/jira_service_close_issue.png)bin88433 -> 88433 bytes
-rw-r--r--doc/project_services/img/jira_service_page.pngbin0 -> 35496 bytes
-rw-r--r--doc/project_services/img/jira_submit_gitlab_merge_request.pngbin0 -> 63063 bytes
-rw-r--r--doc/project_services/img/jira_user_management_link.pngbin0 -> 58211 bytes
-rw-r--r--doc/project_services/img/jira_workflow_screenshot.png (renamed from doc/integration/jira_workflow_screenshot.png)bin121534 -> 121534 bytes
-rw-r--r--doc/project_services/img/redmine_configuration.pngbin0 -> 21061 bytes
-rw-r--r--doc/project_services/img/services_templates_redmine_example.pngbin0 -> 17351 bytes
-rw-r--r--doc/project_services/jira.md234
-rw-r--r--doc/project_services/project_services.md49
-rw-r--r--doc/project_services/redmine.md21
-rw-r--r--doc/project_services/services_templates.md25
-rw-r--r--doc/raketasks/README.md2
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/raketasks/web_hooks.md14
-rw-r--r--doc/release/patch.md2
-rw-r--r--doc/release/security.md2
-rw-r--r--doc/security/README.md4
-rw-r--r--doc/security/img/two_factor_authentication_settings.pngbin0 -> 20399 bytes
-rw-r--r--doc/security/two_factor_authentication.md17
-rw-r--r--doc/security/webhooks.md12
-rw-r--r--doc/ssh/README.md32
-rw-r--r--doc/system_hooks/system_hooks.md56
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md6
-rw-r--r--doc/update/8.2-to-8.3.md2
-rw-r--r--doc/update/8.3-to-8.4.md124
-rw-r--r--doc/update/8.4-to-8.5.md145
-rw-r--r--doc/update/8.5-to-8.6.md164
-rw-r--r--doc/update/README.md1
-rw-r--r--doc/update/patch_versions.md7
-rw-r--r--doc/update/upgrader.md2
-rw-r--r--doc/web_hooks/web_hooks.md278
-rw-r--r--doc/workflow/README.md5
-rw-r--r--doc/workflow/add-user/add-user.md90
-rw-r--r--doc/workflow/add-user/images/add-members.pngbin2361 -> 0 bytes
-rw-r--r--doc/workflow/add-user/images/new-member.pngbin12038 -> 0 bytes
-rw-r--r--doc/workflow/add-user/images/select-project.pngbin4042 -> 0 bytes
-rw-r--r--doc/workflow/add-user/img/add_new_user_to_project_settings.pngbin0 -> 22822 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_accept.pngbin0 -> 10833 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_ready.pngbin0 -> 16177 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_search.pngbin0 -> 15889 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_give_permissions.pngbin0 -> 22089 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_import_members_from_another_project.pngbin0 -> 18897 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_imported_members.pngbin0 -> 23897 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_list_members.pngbin0 -> 15732 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_members_menu.png (renamed from doc/workflow/add-user/images/members.png)bin8295 -> 8295 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_search_people.pngbin0 -> 13518 bytes
-rw-r--r--doc/workflow/file_finder.md46
-rw-r--r--doc/workflow/forking/fork_button.pngbin68271 -> 0 bytes
-rw-r--r--doc/workflow/forking/groups.pngbin98109 -> 0 bytes
-rw-r--r--doc/workflow/forking_workflow.md59
-rw-r--r--doc/workflow/gitlab_flow.md20
-rw-r--r--doc/workflow/groups/max_access_level.pngbin0 -> 135354 bytes
-rw-r--r--doc/workflow/groups/other_group_sees_shared_project.pngbin0 -> 118382 bytes
-rw-r--r--doc/workflow/groups/share_project_with_groups.pngbin0 -> 118868 bytes
-rw-r--r--doc/workflow/img/file_finder_find_button.pngbin0 -> 30974 bytes
-rw-r--r--doc/workflow/img/file_finder_find_file.pngbin0 -> 42658 bytes
-rw-r--r--doc/workflow/img/forking_workflow_choose_namespace.pngbin0 -> 70405 bytes
-rw-r--r--doc/workflow/img/forking_workflow_fork_button.pngbin0 -> 26438 bytes
-rw-r--r--doc/workflow/img/forking_workflow_path_taken_error.pngbin0 -> 22380 bytes
-rw-r--r--doc/workflow/img/revert_changes_commit.pngbin0 -> 360098 bytes
-rw-r--r--doc/workflow/img/revert_changes_commit_modal.pngbin0 -> 327932 bytes
-rw-r--r--doc/workflow/img/revert_changes_mr.pngbin0 -> 367881 bytes
-rw-r--r--doc/workflow/img/revert_changes_mr_modal.pngbin0 -> 335010 bytes
-rw-r--r--doc/workflow/img/todos_icon.pngbin0 -> 7394 bytes
-rw-r--r--doc/workflow/img/todos_index.pngbin0 -> 184839 bytes
-rw-r--r--doc/workflow/img/web_editor_new_branch_dropdown.pngbin0 -> 34233 bytes
-rw-r--r--doc/workflow/img/web_editor_new_branch_page.pngbin0 -> 21618 bytes
-rw-r--r--doc/workflow/img/web_editor_new_directory_dialog.pngbin0 -> 26145 bytes
-rw-r--r--doc/workflow/img/web_editor_new_directory_dropdown.pngbin0 -> 33714 bytes
-rw-r--r--doc/workflow/img/web_editor_new_file_dropdown.pngbin0 -> 34978 bytes
-rw-r--r--doc/workflow/img/web_editor_new_file_editor.pngbin0 -> 128658 bytes
-rw-r--r--doc/workflow/img/web_editor_new_push_widget.pngbin0 -> 11220 bytes
-rw-r--r--doc/workflow/img/web_editor_new_tag_dropdown.pngbin0 -> 33753 bytes
-rw-r--r--doc/workflow/img/web_editor_new_tag_page.pngbin0 -> 75536 bytes
-rw-r--r--doc/workflow/img/web_editor_start_new_merge_request.pngbin0 -> 14352 bytes
-rw-r--r--doc/workflow/img/web_editor_upload_file_dialog.pngbin0 -> 46219 bytes
-rw-r--r--doc/workflow/img/web_editor_upload_file_dropdown.pngbin0 -> 34835 bytes
-rw-r--r--doc/workflow/importing/github_importer/importer.pngbin39335 -> 0 bytes
-rw-r--r--doc/workflow/importing/github_importer/new_project_page.pngbin46276 -> 0 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_importer.pngbin0 -> 28033 bytes
-rw-r--r--doc/workflow/importing/img/import_projects_from_github_new_project_page.pngbin0 -> 17225 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md2
-rw-r--r--doc/workflow/importing/import_projects_from_github.md46
-rw-r--r--doc/workflow/importing/migrating_from_svn.md1
-rw-r--r--doc/workflow/lfs/lfs_administration.md8
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md85
-rw-r--r--doc/workflow/protected_branches.md4
-rw-r--r--doc/workflow/revert_changes.md64
-rw-r--r--doc/workflow/share_projects_with_other_groups.md30
-rw-r--r--doc/workflow/share_with_group.md13
-rw-r--r--doc/workflow/share_with_group.pngbin0 -> 53784 bytes
-rw-r--r--doc/workflow/shortcuts.pngbin78736 -> 25005 bytes
-rw-r--r--doc/workflow/todos.md73
-rw-r--r--doc/workflow/web_editor.md126
-rw-r--r--doc/workflow/web_editor/edit_file.pngbin89039 -> 0 bytes
-rw-r--r--doc/workflow/web_editor/empty_project.pngbin122296 -> 0 bytes
-rw-r--r--doc/workflow/web_editor/new_file.pngbin85526 -> 0 bytes
-rw-r--r--doc/workflow/web_editor/show_file.pngbin111479 -> 0 bytes
-rw-r--r--doc_styleguide.md25
-rw-r--r--features/admin/appearance.feature37
-rw-r--r--features/admin/broadcast_messages.feature26
-rw-r--r--features/admin/groups.feature5
-rw-r--r--features/admin/spam_logs.feature8
-rw-r--r--features/dashboard/archived_projects.feature5
-rw-r--r--features/dashboard/dashboard.feature30
-rw-r--r--features/dashboard/event_filters.feature12
-rw-r--r--features/dashboard/todos.feature38
-rw-r--r--features/explore/groups.feature15
-rw-r--r--features/explore/projects.feature3
-rw-r--r--features/group/milestones.feature17
-rw-r--r--features/groups.feature20
-rw-r--r--features/login_form.feature5
-rw-r--r--features/profile/profile.feature3
-rw-r--r--features/profile/ssh_keys.feature2
-rw-r--r--features/project/badges/build.feature27
-rw-r--r--features/project/builds/artifacts.feature62
-rw-r--r--features/project/builds/permissions.feature53
-rw-r--r--features/project/builds/summary.feature26
-rw-r--r--features/project/commits/commits.feature41
-rw-r--r--features/project/commits/revert.feature28
-rw-r--r--features/project/find_file.feature42
-rw-r--r--features/project/fork.feature33
-rw-r--r--features/project/group_links.feature16
-rw-r--r--features/project/issues/award_emoji.feature19
-rw-r--r--features/project/issues/issues.feature41
-rw-r--r--features/project/issues/references.feature33
-rw-r--r--features/project/labels.feature15
-rw-r--r--features/project/merge_requests.feature64
-rw-r--r--features/project/merge_requests/references.feature31
-rw-r--r--features/project/merge_requests/revert.feature30
-rw-r--r--features/project/milestone.feature24
-rw-r--r--features/project/network_graph.feature3
-rw-r--r--features/project/project.feature6
-rw-r--r--features/project/source/browse_files.feature10
-rw-r--r--features/project/team_management.feature5
-rw-r--r--features/project/wiki.feature5
-rw-r--r--features/search.feature22
-rw-r--r--features/steps/admin/appearance.rb72
-rw-r--r--features/steps/admin/broadcast_messages.rb51
-rw-r--r--features/steps/admin/groups.rb19
-rw-r--r--features/steps/admin/spam_logs.rb28
-rw-r--r--features/steps/dashboard/archived_projects.rb4
-rw-r--r--features/steps/dashboard/dashboard.rb1
-rw-r--r--features/steps/dashboard/issues.rb12
-rw-r--r--features/steps/dashboard/merge_requests.rb11
-rw-r--r--features/steps/dashboard/todos.rb127
-rw-r--r--features/steps/explore/projects.rb2
-rw-r--r--features/steps/group/milestones.rb49
-rw-r--r--features/steps/groups.rb35
-rw-r--r--features/steps/login_form.rb25
-rw-r--r--features/steps/profile/profile.rb28
-rw-r--r--features/steps/profile/ssh_keys.rb4
-rw-r--r--features/steps/project/active_tab.rb4
-rw-r--r--features/steps/project/badges/build.rb32
-rw-r--r--features/steps/project/builds/artifacts.rb86
-rw-r--r--features/steps/project/builds/permissions.rb7
-rw-r--r--features/steps/project/builds/summary.rb39
-rw-r--r--features/steps/project/commits/commits.rb73
-rw-r--r--features/steps/project/commits/revert.rb40
-rw-r--r--features/steps/project/fork.rb50
-rw-r--r--features/steps/project/forked_merge_requests.rb4
-rw-r--r--features/steps/project/hooks.rb4
-rw-r--r--features/steps/project/issues/award_emoji.rb50
-rw-r--r--features/steps/project/issues/filter_labels.rb5
-rw-r--r--features/steps/project/issues/issues.rb76
-rw-r--r--features/steps/project/issues/milestones.rb2
-rw-r--r--features/steps/project/issues/references.rb7
-rw-r--r--features/steps/project/labels.rb34
-rw-r--r--features/steps/project/merge_requests.rb138
-rw-r--r--features/steps/project/merge_requests/acceptance.rb2
-rw-r--r--features/steps/project/merge_requests/references.rb7
-rw-r--r--features/steps/project/merge_requests/revert.rb56
-rw-r--r--features/steps/project/network_graph.rb9
-rw-r--r--features/steps/project/project.rb10
-rw-r--r--features/steps/project/project_find_file.rb73
-rw-r--r--features/steps/project/project_group_links.rb50
-rw-r--r--features/steps/project/project_milestone.rb59
-rw-r--r--features/steps/project/snippets.rb2
-rw-r--r--features/steps/project/source/browse_files.rb19
-rw-r--r--features/steps/project/source/markdown_render.rb18
-rw-r--r--features/steps/project/team_management.rb19
-rw-r--r--features/steps/project/wiki.rb18
-rw-r--r--features/steps/search.rb11
-rw-r--r--features/steps/shared/active_tab.rb4
-rw-r--r--features/steps/shared/builds.rb78
-rw-r--r--features/steps/shared/diff_note.rb16
-rw-r--r--features/steps/shared/issuable.rb191
-rw-r--r--features/steps/shared/note.rb10
-rw-r--r--features/steps/shared/paths.rb47
-rw-r--r--features/steps/shared/project.rb69
-rw-r--r--features/steps/shared/user.rb16
-rw-r--r--features/support/capybara.rb12
-rw-r--r--features/support/env.rb1
-rw-r--r--features/support/rerun.rb14
-rw-r--r--features/user.feature13
-rw-r--r--fixtures/emojis/aliases.json138
-rwxr-xr-xfixtures/emojis/generate_aliases.rb18
-rw-r--r--fixtures/emojis/index.json20099
-rw-r--r--lib/api/api.rb3
-rw-r--r--lib/api/builds.rb204
-rw-r--r--lib/api/commit_statuses.rb10
-rw-r--r--lib/api/commits.rb6
-rw-r--r--lib/api/entities.rb72
-rw-r--r--lib/api/files.rb6
-rw-r--r--lib/api/helpers.rb55
-rw-r--r--lib/api/internal.rb13
-rw-r--r--lib/api/issues.rb26
-rw-r--r--lib/api/merge_requests.rb366
-rw-r--r--lib/api/notes.rb21
-rw-r--r--lib/api/projects.rb70
-rw-r--r--lib/api/repositories.rb11
-rw-r--r--lib/api/runners.rb175
-rw-r--r--lib/api/tags.rb21
-rw-r--r--lib/api/triggers.rb69
-rw-r--r--lib/api/users.rb24
-rw-r--r--lib/api/variables.rb95
-rw-r--r--lib/backup/manager.rb3
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/cross_project_reference.rb2
-rw-r--r--lib/banzai/filter.rb1
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb54
-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_issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/external_link_filter.rb1
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb174
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb7
-rw-r--r--lib/banzai/filter/label_reference_filter.rb81
-rw-r--r--lib/banzai/filter/markdown_filter.rb1
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb2
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb22
-rw-r--r--lib/banzai/filter/redactor_filter.rb3
-rw-r--r--lib/banzai/filter/reference_filter.rb11
-rw-r--r--lib/banzai/filter/reference_gatherer_filter.rb3
-rw-r--r--lib/banzai/filter/relative_link_filter.rb3
-rw-r--r--lib/banzai/filter/sanitization_filter.rb29
-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.rb1
-rw-r--r--lib/banzai/filter/task_list_filter.rb14
-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.rb28
-rw-r--r--lib/banzai/filter_array.rb27
-rw-r--r--lib/banzai/lazy_reference.rb2
-rw-r--r--lib/banzai/pipeline.rb2
-rw-r--r--lib/banzai/pipeline/asciidoc_pipeline.rb13
-rw-r--r--lib/banzai/pipeline/atom_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/base_pipeline.rb3
-rw-r--r--lib/banzai/pipeline/broadcast_message_pipeline.rb16
-rw-r--r--lib/banzai/pipeline/combined_pipeline.rb4
-rw-r--r--lib/banzai/pipeline/description_pipeline.rb15
-rw-r--r--lib/banzai/pipeline/email_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/full_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb5
-rw-r--r--lib/banzai/pipeline/note_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/plain_markdown_pipeline.rb4
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb4
-rw-r--r--lib/banzai/pipeline/pre_process_pipeline.rb17
-rw-r--r--lib/banzai/pipeline/reference_extraction_pipeline.rb4
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb18
-rw-r--r--lib/banzai/pipeline/wiki_pipeline.rb12
-rw-r--r--lib/banzai/querying.rb18
-rw-r--r--lib/banzai/reference_extractor.rb2
-rw-r--r--lib/banzai/renderer.rb10
-rw-r--r--lib/ci/api/api.rb2
-rw-r--r--lib/ci/api/builds.rb32
-rw-r--r--lib/ci/api/entities.rb17
-rw-r--r--lib/ci/api/helpers.rb10
-rw-r--r--lib/ci/api/runners.rb1
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb37
-rw-r--r--lib/ci/status.rb4
-rw-r--r--lib/gitlab/akismet_helper.rb39
-rw-r--r--lib/gitlab/asciidoc.rb4
-rw-r--r--lib/gitlab/backend/shell.rb16
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb51
-rw-r--r--lib/gitlab/blame.rb55
-rw-r--r--lib/gitlab/build_data_builder.rb1
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb111
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb126
-rw-r--r--lib/gitlab/compare_result.rb9
-rw-r--r--lib/gitlab/contributions_calendar.rb4
-rw-r--r--lib/gitlab/current_settings.rb36
-rw-r--r--lib/gitlab/database.rb34
-rw-r--r--lib/gitlab/devise_failure.rb23
-rw-r--r--lib/gitlab/diff/file.rb27
-rw-r--r--lib/gitlab/diff/highlight.rb77
-rw-r--r--lib/gitlab/diff/inline_diff.rb88
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb115
-rw-r--r--lib/gitlab/diff/line.rb3
-rw-r--r--lib/gitlab/diff/parallel_diff.rb119
-rw-r--r--lib/gitlab/diff/parser.rb92
-rw-r--r--lib/gitlab/email/message/repository_push.rb3
-rw-r--r--lib/gitlab/email/receiver.rb7
-rw-r--r--lib/gitlab/exclusive_lease.rb41
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb4
-rw-r--r--lib/gitlab/git.rb6
-rw-r--r--lib/gitlab/git_post_receive.rb60
-rw-r--r--lib/gitlab/github_import/base_formatter.rb21
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb45
-rw-r--r--lib/gitlab/github_import/importer.rb93
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb66
-rw-r--r--lib/gitlab/github_import/project_creator.rb3
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb105
-rw-r--r--lib/gitlab/github_import/wiki_formatter.rb19
-rw-r--r--lib/gitlab/gitlab_import/importer.rb2
-rw-r--r--lib/gitlab/highlight.rb39
-rw-r--r--lib/gitlab/inline_diff.rb104
-rw-r--r--lib/gitlab/ldap/access.rb14
-rw-r--r--lib/gitlab/ldap/adapter.rb24
-rw-r--r--lib/gitlab/ldap/config.rb4
-rw-r--r--lib/gitlab/ldap/user.rb33
-rw-r--r--lib/gitlab/markdown/pipeline.rb2
-rw-r--r--lib/gitlab/metrics.rb51
-rw-r--r--lib/gitlab/metrics/instrumentation.rb24
-rw-r--r--lib/gitlab/metrics/metric.rb7
-rw-r--r--lib/gitlab/metrics/obfuscated_sql.rb47
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb10
-rw-r--r--lib/gitlab/metrics/sampler.rb49
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb7
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb11
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb30
-rw-r--r--lib/gitlab/metrics/transaction.rb61
-rw-r--r--lib/gitlab/middleware/go.rb50
-rw-r--r--lib/gitlab/note_data_builder.rb11
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb8
-rw-r--r--lib/gitlab/o_auth/user.rb32
-rw-r--r--lib/gitlab/other_markup.rb24
-rw-r--r--lib/gitlab/project_search_results.rb11
-rw-r--r--lib/gitlab/push_data_builder.rb20
-rw-r--r--lib/gitlab/redis_config.rb30
-rw-r--r--lib/gitlab/reference_extractor.rb4
-rw-r--r--lib/gitlab/regex.rb12
-rw-r--r--lib/gitlab/saml/user.rb47
-rw-r--r--lib/gitlab/search_results.rb28
-rw-r--r--lib/gitlab/snippet_search_results.rb100
-rw-r--r--lib/gitlab/user_access.rb2
-rw-r--r--lib/gitlab/workhorse.rb40
-rwxr-xr-xlib/support/init.d/gitlab13
-rwxr-xr-xlib/support/init.d/gitlab.default.example16
-rw-r--r--lib/support/nginx/gitlab11
-rw-r--r--lib/support/nginx/gitlab-ssl10
-rw-r--r--lib/tasks/brakeman.rake2
-rw-r--r--lib/tasks/cache.rake20
-rw-r--r--lib/tasks/gemojione.rake122
-rw-r--r--lib/tasks/gitlab/check.rake46
-rw-r--r--lib/tasks/gitlab/task_helpers.rake5
-rw-r--r--lib/tasks/gitlab/web_hook.rake14
-rw-r--r--lib/tasks/scss-lint.rake10
-rw-r--r--lib/tasks/spec.rake13
-rw-r--r--lib/tasks/spinach.rake62
-rw-r--r--lib/version_check.rb2
-rw-r--r--public/404.html44
-rw-r--r--public/422.html45
-rw-r--r--public/500.html44
-rw-r--r--public/502.html44
-rw-r--r--public/deploy.html41
-rw-r--r--public/static.css36
-rwxr-xr-xscripts/ci/prepare_build.sh22
-rwxr-xr-xscripts/notify_slack.sh13
-rwxr-xr-xscripts/prepare_build.sh30
-rw-r--r--spec/benchmarks/finders/issues_finder_spec.rb55
-rw-r--r--spec/benchmarks/finders/trending_projects_finder_spec.rb14
-rw-r--r--spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb41
-rw-r--r--spec/benchmarks/models/milestone_spec.rb17
-rw-r--r--spec/benchmarks/models/project_spec.rb50
-rw-r--r--spec/benchmarks/models/project_team_spec.rb23
-rw-r--r--spec/benchmarks/models/user_spec.rb78
-rw-r--r--spec/benchmarks/services/projects/create_service_spec.rb28
-rw-r--r--spec/config/mail_room_spec.rb56
-rw-r--r--spec/controllers/abuse_reports_controller_spec.rb80
-rw-r--r--spec/controllers/admin/identities_controller_spec.rb26
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb37
-rw-r--r--spec/controllers/admin/users_controller_spec.rb35
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb2
-rw-r--r--spec/controllers/blame_controller_spec.rb43
-rw-r--r--spec/controllers/branches_controller_spec.rb104
-rw-r--r--spec/controllers/ci/projects_controller_spec.rb53
-rw-r--r--spec/controllers/commit_controller_spec.rb51
-rw-r--r--spec/controllers/groups_controller_spec.rb23
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb (renamed from spec/controllers/profile_keys_controller_spec.rb)0
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb29
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb134
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb37
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb (renamed from spec/controllers/commits_controller_spec.rb)0
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb8
-rw-r--r--spec/controllers/projects/find_file_controller_spec.rb66
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb72
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb121
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb162
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb62
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb42
-rw-r--r--spec/controllers/projects_controller_spec.rb21
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb26
-rw-r--r--spec/controllers/users_controller_spec.rb18
-rw-r--r--spec/factories.rb215
-rw-r--r--spec/factories/abuse_reports.rb2
-rw-r--r--spec/factories/appearances.rb8
-rw-r--r--spec/factories/broadcast_messages.rb20
-rw-r--r--spec/factories/ci/builds.rb76
-rw-r--r--spec/factories/ci/commits.rb1
-rw-r--r--spec/factories/ci/runner_projects.rb2
-rw-r--r--spec/factories/ci/runners.rb12
-rw-r--r--spec/factories/ci/trigger_requests.rb4
-rw-r--r--spec/factories/ci/triggers.rb2
-rw-r--r--spec/factories/ci/variables.rb20
-rw-r--r--spec/factories/commit_statuses.rb8
-rw-r--r--spec/factories/deploy_keys_projects.rb6
-rw-r--r--spec/factories/emails.rb6
-rw-r--r--spec/factories/events.rb10
-rw-r--r--spec/factories/forked_project_links.rb2
-rw-r--r--spec/factories/groups.rb7
-rw-r--r--spec/factories/identities.rb6
-rw-r--r--spec/factories/issues.rb22
-rw-r--r--spec/factories/keys.rb24
-rw-r--r--spec/factories/label_links.rb2
-rw-r--r--spec/factories/labels.rb4
-rw-r--r--spec/factories/lfs_objects.rb2
-rw-r--r--spec/factories/lfs_objects_projects.rb2
-rw-r--r--spec/factories/merge_requests.rb62
-rw-r--r--spec/factories/milestones.rb12
-rw-r--r--spec/factories/namespaces.rb7
-rw-r--r--spec/factories/notes.rb16
-rw-r--r--spec/factories/personal_snippets.rb4
-rw-r--r--spec/factories/project_group_links.rb6
-rw-r--r--spec/factories/project_hooks.rb5
-rw-r--r--spec/factories/project_members.rb27
-rw-r--r--spec/factories/project_snippets.rb5
-rw-r--r--spec/factories/projects.rb7
-rw-r--r--spec/factories/protected_branches.rb6
-rw-r--r--spec/factories/releases.rb2
-rw-r--r--spec/factories/sent_notifications.rb8
-rw-r--r--spec/factories/service_hooks.rb6
-rw-r--r--spec/factories/services.rb5
-rw-r--r--spec/factories/snippets.rb28
-rw-r--r--spec/factories/spam_logs.rb9
-rw-r--r--spec/factories/system_hooks.rb5
-rw-r--r--spec/factories/todos.rb34
-rw-r--r--spec/factories/users.rb43
-rw-r--r--spec/features/admin/admin_builds_spec.rb119
-rw-r--r--spec/features/builds_spec.rb33
-rw-r--r--spec/features/ci_lint_spec.rb8
-rw-r--r--spec/features/commits_spec.rb169
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb11
-rw-r--r--spec/features/issues/new_branch_button_spec.rb49
-rw-r--r--spec/features/issues_spec.rb12
-rw-r--r--spec/features/login_spec.rb36
-rw-r--r--spec/features/markdown_spec.rb64
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb5
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb8
-rw-r--r--spec/features/projects_spec.rb25
-rw-r--r--spec/features/runners_spec.rb10
-rw-r--r--spec/features/security/project/internal_access_spec.rb57
-rw-r--r--spec/features/security/project/private_access_spec.rb52
-rw-r--r--spec/features/security/project/public_access_spec.rb95
-rw-r--r--spec/finders/groups_finder_spec.rb48
-rw-r--r--spec/finders/joined_groups_finder_spec.rb49
-rw-r--r--spec/finders/projects_finder_spec.rb34
-rw-r--r--spec/finders/snippets_finder_spec.rb25
-rw-r--r--spec/fixtures/ci_build_artifacts.zipbin0 -> 106365 bytes
-rw-r--r--spec/fixtures/ci_build_artifacts_metadata.gzbin0 -> 461 bytes
-rw-r--r--spec/fixtures/logo_sample.svg (renamed from public/logo.svg)17
-rw-r--r--spec/fixtures/mail_room_disabled.yml11
-rw-r--r--spec/fixtures/mail_room_enabled.yml11
-rw-r--r--spec/fixtures/markdown.md.erb18
-rw-r--r--spec/fixtures/parallel_diff_result.yml324
-rw-r--r--spec/helpers/application_helper_spec.rb16
-rw-r--r--spec/helpers/blob_helper_spec.rb63
-rw-r--r--spec/helpers/broadcast_messages_helper_spec.rb62
-rw-r--r--spec/helpers/diff_helper_spec.rb122
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb7
-rw-r--r--spec/helpers/labels_helper_spec.rb38
-rw-r--r--spec/helpers/page_layout_helper_spec.rb64
-rw-r--r--spec/helpers/search_helper_spec.rb6
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb6
-rw-r--r--spec/initializers/settings_spec.rb44
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js.coffee11
-rw-r--r--spec/javascripts/fixtures/behaviors/quick_submit.html.haml6
-rw-r--r--spec/javascripts/fixtures/project_title.html.haml7
-rw-r--r--spec/javascripts/fixtures/projects.json1
-rw-r--r--spec/javascripts/fixtures/zen_mode.html.haml9
-rw-r--r--spec/javascripts/issue_spec.js.coffee34
-rw-r--r--spec/javascripts/project_title_spec.js.coffee46
-rw-r--r--spec/javascripts/zen_mode_spec.js.coffee26
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb104
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb27
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb75
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb72
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb63
-rw-r--r--spec/lib/banzai/filter/task_list_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb53
-rw-r--r--spec/lib/banzai/filter_array_spec.rb39
-rw-r--r--spec/lib/banzai/pipeline/description_pipeline_spec.rb37
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb53
-rw-r--r--spec/lib/banzai/querying_spec.rb13
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb148
-rw-r--r--spec/lib/ci/status_spec.rb94
-rw-r--r--spec/lib/gitlab/akismet_helper_spec.rb35
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb16
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb88
-rw-r--r--spec/lib/gitlab/blame_spec.rb24
-rw-r--r--spec/lib/gitlab/build_data_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb173
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb97
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb12
-rw-r--r--spec/lib/gitlab/database_spec.rb52
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb16
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb77
-rw-r--r--spec/lib/gitlab/diff/inline_diff_marker_spec.rb29
-rw-r--r--spec/lib/gitlab/diff/inline_diff_spec.rb40
-rw-r--r--spec/lib/gitlab/diff/parallel_diff_spec.rb22
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb9
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb2
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb11
-rw-r--r--spec/lib/gitlab/exclusive_lease_spec.rb21
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb80
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb139
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb185
-rw-r--r--spec/lib/gitlab/github_import/wiki_formatter_spec.rb22
-rw-r--r--spec/lib/gitlab/highlight_spec.rb21
-rw-r--r--spec/lib/gitlab/inline_diff_spec.rb39
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb35
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb28
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb16
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb3
-rw-r--r--spec/lib/gitlab/metrics/obfuscated_sql_spec.rb93
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/sampler_spec.rb60
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb17
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb12
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb33
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb67
-rw-r--r--spec/lib/gitlab/metrics_spec.rb27
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb30
-rw-r--r--spec/lib/gitlab/note_data_builder_spec.rb42
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb26
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb69
-rw-r--r--spec/lib/gitlab/push_data_builder_spec.rb27
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb2
-rw-r--r--spec/lib/gitlab/regex_spec.rb8
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb271
-rw-r--r--spec/lib/gitlab/search_results_spec.rb144
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb25
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb18
-rw-r--r--spec/mailers/abuse_report_mailer_spec.rb38
-rw-r--r--spec/mailers/emails/builds_spec.rb65
-rw-r--r--spec/mailers/emails/profile_spec.rb111
-rw-r--r--spec/mailers/notify_spec.rb347
-rw-r--r--spec/mailers/shared/notify.rb121
-rw-r--r--spec/models/abuse_report_spec.rb35
-rw-r--r--spec/models/appearance_spec.rb10
-rw-r--r--spec/models/application_setting_spec.rb79
-rw-r--r--spec/models/blob_spec.rb81
-rw-r--r--spec/models/broadcast_message_spec.rb72
-rw-r--r--spec/models/build_spec.rb216
-rw-r--r--spec/models/ci/build_spec.rb22
-rw-r--r--spec/models/ci/commit_spec.rb75
-rw-r--r--spec/models/ci/runner_project_spec.rb11
-rw-r--r--spec/models/ci/runner_spec.rb42
-rw-r--r--spec/models/ci/trigger_spec.rb13
-rw-r--r--spec/models/ci/variable_spec.rb3
-rw-r--r--spec/models/commit_spec.rb47
-rw-r--r--spec/models/commit_status_spec.rb1
-rw-r--r--spec/models/concerns/case_sensitivity_spec.rb12
-rw-r--r--spec/models/concerns/issuable_spec.rb116
-rw-r--r--spec/models/concerns/mentionable_spec.rb5
-rw-r--r--spec/models/concerns/milestoneish_spec.rb104
-rw-r--r--spec/models/concerns/subscribable_spec.rb57
-rw-r--r--spec/models/email_spec.rb22
-rw-r--r--spec/models/event_spec.rb37
-rw-r--r--spec/models/external_issue_spec.rb15
-rw-r--r--spec/models/external_wiki_service_spec.rb1
-rw-r--r--spec/models/generic_commit_status_spec.rb1
-rw-r--r--spec/models/group_spec.rb40
-rw-r--r--spec/models/hooks/project_hook_spec.rb4
-rw-r--r--spec/models/hooks/service_hook_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb6
-rw-r--r--spec/models/hooks/web_hook_spec.rb34
-rw-r--r--spec/models/identity_spec.rb38
-rw-r--r--spec/models/issue_spec.rb42
-rw-r--r--spec/models/label_spec.rb30
-rw-r--r--spec/models/member_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb194
-rw-r--r--spec/models/milestone_spec.rb65
-rw-r--r--spec/models/namespace_spec.rb30
-rw-r--r--spec/models/note_spec.rb137
-rw-r--r--spec/models/project_group_link_spec.rb17
-rw-r--r--spec/models/project_services/asana_service_spec.rb77
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb23
-rw-r--r--spec/models/project_snippet_spec.rb1
-rw-r--r--spec/models/project_spec.rb164
-rw-r--r--spec/models/project_team_spec.rb70
-rw-r--r--spec/models/project_wiki_spec.rb7
-rw-r--r--spec/models/repository_spec.rb597
-rw-r--r--spec/models/service_spec.rb1
-rw-r--r--spec/models/snippet_spec.rb45
-rw-r--r--spec/models/spam_log_spec.rb25
-rw-r--r--spec/models/todo_spec.rb69
-rw-r--r--spec/models/tree_spec.rb64
-rw-r--r--spec/models/user_spec.rb260
-rw-r--r--spec/models/wiki_page_spec.rb32
-rw-r--r--spec/requests/api/branches_spec.rb4
-rw-r--r--spec/requests/api/builds_spec.rb244
-rw-r--r--spec/requests/api/commit_status_spec.rb188
-rw-r--r--spec/requests/api/commits_spec.rb4
-rw-r--r--spec/requests/api/fork_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb12
-rw-r--r--spec/requests/api/issues_spec.rb149
-rw-r--r--spec/requests/api/merge_requests_spec.rb100
-rw-r--r--spec/requests/api/notes_spec.rb56
-rw-r--r--spec/requests/api/project_members_spec.rb4
-rw-r--r--spec/requests/api/project_snippets_spec.rb18
-rw-r--r--spec/requests/api/projects_spec.rb86
-rw-r--r--spec/requests/api/repositories_spec.rb17
-rw-r--r--spec/requests/api/runners_spec.rb464
-rw-r--r--spec/requests/api/tags_spec.rb25
-rw-r--r--spec/requests/api/triggers_spec.rb139
-rw-r--r--spec/requests/api/users_spec.rb52
-rw-r--r--spec/requests/api/variables_spec.rb182
-rw-r--r--spec/requests/ci/api/builds_spec.rb191
-rw-r--r--spec/requests/ci/api/runners_spec.rb14
-rw-r--r--spec/routing/project_routing_spec.rb25
-rw-r--r--spec/routing/routing_spec.rb5
-rw-r--r--spec/services/archive_repository_service_spec.rb25
-rw-r--r--spec/services/ci/create_builds_service_spec.rb28
-rw-r--r--spec/services/delete_tag_service_spec.rb26
-rw-r--r--spec/services/delete_user_service_spec.rb58
-rw-r--r--spec/services/git_push_service_spec.rb173
-rw-r--r--spec/services/git_tag_push_service_spec.rb4
-rw-r--r--spec/services/issues/close_service_spec.rb6
-rw-r--r--spec/services/issues/create_service_spec.rb23
-rw-r--r--spec/services/issues/update_service_spec.rb114
-rw-r--r--spec/services/merge_requests/close_service_spec.rb5
-rw-r--r--spec/services/merge_requests/create_service_spec.rb42
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb84
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb124
-rw-r--r--spec/services/notes/create_service_spec.rb4
-rw-r--r--spec/services/notes/post_process_service_spec.rb27
-rw-r--r--spec/services/notes/update_service_spec.rb45
-rw-r--r--spec/services/notification_service_spec.rb161
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb79
-rw-r--r--spec/services/projects/create_service_spec.rb1
-rw-r--r--spec/services/projects/download_service_spec.rb24
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb48
-rw-r--r--spec/services/projects/import_service_spec.rb106
-rw-r--r--spec/services/projects/update_service_spec.rb4
-rw-r--r--spec/services/repair_ldap_blocked_user_service_spec.rb23
-rw-r--r--spec/services/system_hooks_service_spec.rb47
-rw-r--r--spec/services/system_note_service_spec.rb33
-rw-r--r--spec/services/todo_service_spec.rb274
-rw-r--r--spec/spec_helper.rb13
-rw-r--r--spec/support/api/pagination_shared_examples.rb20
-rw-r--r--spec/support/capybara.rb10
-rw-r--r--spec/support/email_format_shared_examples.rb44
-rw-r--r--spec/support/email_helpers.rb13
-rw-r--r--spec/support/filter_spec_helper.rb4
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci.yml8
-rw-r--r--spec/support/markdown_feature.rb12
-rw-r--r--spec/support/matchers/access_matchers.rb2
-rw-r--r--spec/support/matchers/benchmark_matchers.rb61
-rw-r--r--spec/support/matchers/markdown_matchers.rb27
-rw-r--r--spec/support/mentionable_shared_examples.rb2
-rw-r--r--spec/support/project_hook_data_shared_example.rb27
-rw-r--r--spec/support/test_env.rb17
-rw-r--r--spec/support/wait_for_ajax.rb2
-rw-r--r--spec/support/workhorse_helpers.rb16
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb37
-rw-r--r--spec/workers/delete_user_worker_spec.rb20
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--spec/workers/repository_fork_worker_spec.rb12
-rw-r--r--spec/workers/repository_import_worker_spec.rb19
-rwxr-xr-xvendor/assets/javascripts/Chart.js3477
-rwxr-xr-xvendor/assets/javascripts/autosize.js243
-rw-r--r--vendor/assets/javascripts/chart-lib.min.js11
-rw-r--r--vendor/assets/javascripts/fuzzaldrin-plus.js1161
-rw-r--r--vendor/assets/javascripts/g.bar-min.js8
-rw-r--r--vendor/assets/javascripts/g.bar.js674
-rw-r--r--vendor/assets/javascripts/g.raphael-min.js7
-rw-r--r--vendor/assets/javascripts/g.raphael.js861
-rw-r--r--vendor/assets/javascripts/jquery.ba-resize.js246
-rw-r--r--vendor/assets/javascripts/jquery.blockUI.js590
-rw-r--r--vendor/assets/javascripts/jquery.history.js1
-rw-r--r--vendor/assets/javascripts/jquery.nicescroll.js3634
-rw-r--r--vendor/assets/javascripts/jquery.nicescroll.min.js118
-rw-r--r--vendor/assets/javascripts/latinise.js11
1704 files changed, 72648 insertions, 17227 deletions
diff --git a/.csscomb.json b/.csscomb.json
new file mode 100644
index 00000000000..e353e6a63d0
--- /dev/null
+++ b/.csscomb.json
@@ -0,0 +1,16 @@
+{
+ "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/.gitignore b/.gitignore
index f5b6427ca03..8f861d76a37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
.sass-cache/
.secret
.vagrant
+.byebug_history
Vagrantfile
backups/*
config/aws.yml
@@ -23,9 +24,11 @@ config/gitlab.yml
config/gitlab_ci.yml
config/initializers/rack_attack.rb
config/initializers/smtp_settings.rb
+config/initializers/relative_url.rb
config/resque.yml
config/unicorn.rb
config/secrets.yml
+config/sidekiq.yml
coverage/*
db/*.sqlite3
db/*.sqlite3-journal
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c23a7a3bf0e..2ad63548d78 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,16 +1,37 @@
-# This file is generated by GitLab CI
+image: "ruby:2.1"
+
+services:
+ - mysql:latest
+ - postgres:latest
+ - redis:latest
+
+cache:
+ key: "ruby21"
+ paths:
+ - vendor
+
+variables:
+ MYSQL_ALLOW_EMPTY_PASSWORD: "1"
+ # retry tests only in CI environment
+ RSPEC_RETRY_RETRY_COUNT: "3"
+
before_script:
- - ./scripts/prepare_build.sh
+ - source ./scripts/prepare_build.sh
- ruby -v
- which ruby
- - gem install bundler --no-ri --no-rdoc
+ - retry gem install bundler --no-ri --no-rdoc
- cp config/gitlab.yml.example config/gitlab.yml
- touch log/application.log
- touch log/test.log
- - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- - bundle exec rake db:reset db:create RAILS_ENV=test
+ - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
+ - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
+
+stages:
+- test
+- notifications
spec:feature:
+ stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
@@ -19,6 +40,7 @@ spec:feature:
- mysql
spec:api:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
tags:
@@ -26,6 +48,7 @@ spec:api:
- mysql
spec:models:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
tags:
@@ -33,6 +56,7 @@ spec:models:
- mysql
spec:lib:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
tags:
@@ -40,21 +64,15 @@ spec:lib:
- mysql
spec:services:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
tags:
- ruby
- mysql
-spec:benchmark:
- script:
- - RAILS_ENV=test bundle exec rake spec:benchmark
- tags:
- - ruby
- - mysql
- allow_failure: true
-
spec:other:
+ stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
tags:
@@ -62,27 +80,34 @@ spec:other:
- mysql
spinach:project:half:
+ stage: test
script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
tags:
- ruby
- mysql
spinach:project:rest:
+ stage: test
script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
tags:
- ruby
- mysql
spinach:other:
+ stage: test
script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
tags:
- ruby
- mysql
teaspoon:
+ stage: test
script:
- RAILS_ENV=test bundle exec teaspoon
tags:
@@ -90,13 +115,23 @@ teaspoon:
- mysql
rubocop:
+ stage: test
script:
- bundle exec rubocop
tags:
- ruby
- mysql
+scss-lint:
+ stage: test
+ script:
+ - bundle exec rake scss_lint
+ tags:
+ - ruby
+ allow_failure: true
+
brakeman:
+ stage: test
script:
- bundle exec rake brakeman
tags:
@@ -104,6 +139,7 @@ brakeman:
- mysql
flog:
+ stage: test
script:
- bundle exec rake flog
tags:
@@ -111,6 +147,7 @@ flog:
- mysql
flay:
+ stage: test
script:
- bundle exec rake flay
tags:
@@ -118,10 +155,165 @@ flay:
- mysql
bundler:audit:
- script:
+ stage: test
+ only:
+ - master
+ script:
- "bundle exec bundle-audit update"
- - "bundle exec bundle-audit check"
+ - "bundle exec bundle-audit check --ignore OSVDB-115941"
tags:
- ruby
- mysql
- allow_failure: true
+
+# Ruby 2.2 jobs
+
+spec:feature:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+spec:api:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+spec:models:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+spec:lib:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+spec:services:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+spec:other:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+spinach:project:half:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+spinach:project:rest:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+spinach:other:ruby22:
+ stage: test
+ image: ruby:2.2
+ only:
+ - master
+ script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
+ cache:
+ key: "ruby22"
+ paths:
+ - vendor
+ tags:
+ - ruby
+ - mysql
+
+
+notify:slack:
+ stage: notifications
+ script:
+ - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
+ when: on_failure
+ only:
+ - master@gitlab-org/gitlab-ce
+ - tags@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - tags@gitlab-org/gitlab-ee
diff --git a/.ruby-version b/.ruby-version
index 04b10b4f150..ebf14b46981 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.1.7
+2.1.8
diff --git a/.scss-lint.yml b/.scss-lint.yml
new file mode 100644
index 00000000000..e350b2073c3
--- /dev/null
+++ b/.scss-lint.yml
@@ -0,0 +1,158 @@
+# Linter Documentation:
+# https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
+
+scss_files: 'app/assets/stylesheets/**/*.scss'
+
+exclude:
+ - 'app/assets/stylesheets/pages/emojis.scss'
+
+linters:
+ BangFormat:
+ enabled: false
+
+ BorderZero:
+ enabled: false
+
+ ColorKeyword:
+ enabled: false
+
+ ColorVariable:
+ enabled: false
+
+ Comment:
+ enabled: false
+
+ DeclarationOrder:
+ enabled: false
+
+ # `scss-lint:disable` control comments should be preceded by a comment
+ # explaining why these linters are being disabled for this file.
+ # See https://github.com/brigade/scss-lint#disabling-linters-via-source for
+ # more information.
+ DisableLinterReason:
+ enabled: true
+
+ DuplicateProperty:
+ enabled: false
+
+ EmptyLineBetweenBlocks:
+ enabled: false
+
+ EmptyRule:
+ enabled: false
+
+ FinalNewline:
+ enabled: false
+
+ # HEX colors should use three-character values where possible.
+ HexLength:
+ enabled: true
+
+ # HEX color values should use lower-case colors to differentiate between
+ # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
+ HexNotation:
+ enabled: true
+
+ IdSelector:
+ enabled: false
+
+ ImportPath:
+ enabled: false
+
+ ImportantRule:
+ enabled: false
+
+ # Indentation should always be done in increments of 2 spaces.
+ Indentation:
+ enabled: true
+ width: 2
+
+ LeadingZero:
+ enabled: false
+
+ MergeableSelector:
+ enabled: false
+
+ NameFormat:
+ enabled: false
+
+ NestingDepth:
+ enabled: false
+
+ PlaceholderInExtend:
+ enabled: false
+
+ PropertySortOrder:
+ enabled: false
+
+ PropertySpelling:
+ enabled: false
+
+ PseudoElement:
+ enabled: false
+
+ QualifyingElement:
+ enabled: false
+
+ SelectorDepth:
+ enabled: false
+
+ # Selectors should always use hyphenated-lowercase, rather than camelCase or
+ # snake_case.
+ SelectorFormat:
+ enabled: true
+ convention: hyphenated_lowercase
+
+ # Prefer the shortest shorthand form possible for properties that support it.
+ Shorthand:
+ enabled: true
+
+ # Each property should have its own line, except in the special case of
+ # single line rulesets.
+ SingleLinePerProperty:
+ enabled: true
+ allow_single_line_rule_sets: true
+
+ SingleLinePerSelector:
+ enabled: false
+
+ SpaceAfterComma:
+ enabled: false
+
+ # Properties should be formatted with a single space separating the colon
+ # from the property's value.
+ SpaceAfterPropertyColon:
+ enabled: true
+
+ # Properties should be formatted with no space between the name and the
+ # colon.
+ SpaceAfterPropertyName:
+ enabled: true
+
+ SpaceAroundOperator:
+ enabled: false
+
+ SpaceBeforeBrace:
+ enabled: false
+
+ StringQuotes:
+ enabled: false
+
+ TrailingSemicolon:
+ enabled: false
+
+ TrailingWhitespace:
+ enabled: false
+
+ UnnecessaryMantissa:
+ enabled: false
+
+ UnnecessaryParentReference:
+ enabled: false
+
+ VendorPrefix:
+ enabled: false
+
+ # Omit length units on zero values, e.g. `0px` vs. `0`.
+ ZeroUnit:
+ enabled: true
diff --git a/CHANGELOG b/CHANGELOG
index b25e0eabd7d..74217e80bfe 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,23 +1,329 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.4.0 (unreleased)
+v 8.6.0 (unreleased)
+ - Add confidential issues
+ - Bump gitlab_git to 9.0.3 (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)
+ - 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
+ - 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
+ - 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)
+ - 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
+ - Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
+ - Rewrite logo to simplify SVG code (Sean Lang)
+ - Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
+ - Ignore jobs that start with `.` (hidden jobs)
+ - Hide builds from project's settings when the feature is disabled
+ - Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
+ - Refactor and greatly improve search performance
+ - Add support for cross-project label references
+ - Ensure "new SSH key" email do not ends up as dead Sidekiq jobs
+ - Update documentation to reflect Guest role not being enforced on internal projects
+ - Allow search for logged out users
+ - Allow to define on which builds the current one depends on
+ - Allow user subscription to a label: get notified for issues/merge requests related to that label (Timothy Andrew)
+ - 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
+ - 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`
+
+v 8.5.8
+ - Bump Git version requirement to 2.7.4
+
+v 8.5.7
+ - Bump Git version requirement to 2.7.3
+
+v 8.5.6
+ - Obtain a lease before querying LDAP
+
+v 8.5.5
+ - Ensure removing a project removes associated Todo entries
+ - Prevent a 500 error in Todos when author was removed
+ - Fix pagination for filtered dashboard and explore pages
+ - Fix "Show all" link behavior
+
+v 8.5.4
+ - Do not cache requests for badges (including builds badge)
+
+v 8.5.3
+ - Flush repository caches before renaming projects
+ - Sort starred projects on dashboard based on last activity by default
+ - Show commit message in JIRA mention comment
+ - Makes issue page and merge request page usable on mobile browsers.
+ - Improved UI for profile settings
+
+v 8.5.2
+ - Fix sidebar overlapping content when screen width was below 1200px
+ - Don't repeat labels listed on Labels tab
+ - Bring the "branded appearance" feature from EE to CE
+ - Fix error 500 when commenting on a commit
+ - Show days remaining instead of elapsed time for Milestone
+ - Fix broken icons on installations with relative URL (Artem Sidorenko)
+ - Fix issue where tag list wasn't refreshed after deleting a tag
+ - Fix import from gitlab.com (KazSawada)
+ - Improve implementation to check read access to forks and add pagination
+ - Don't show any "2FA required" message if it's not actually required
+ - Fix help keyboard shortcut on relative URL setups (Artem Sidorenko)
+ - Update Rails to 4.2.5.2
+ - Fix permissions for deprecated CI build status badge
+ - Don't show "Welcome to GitLab" when the search didn't return any projects
+ - Add Todos documentation
+
+v 8.5.1
+ - Fix group projects styles
+ - Show Crowd login tab when sign in is disabled and Crowd is enabled (Peter Hudec)
+ - Fix a set of small UI glitches in project, profile, and wiki pages
+ - Restrict permissions on public/uploads
+ - Fix the merge request side-by-side view after loading diff results
+ - Fix the look of tooltip for the "Revert" button
+ - Add when the Builds & Runners API changes got introduced
+ - Fix error 500 on some merged merge requests
+ - Fix an issue causing the content of the issuable sidebar to disappear
+ - Fix error 500 when trying to mark an already done todo as "done"
+ - Fix an issue where MRs weren't sortable
+ - Issues can now be dragged & dropped into empty milestone lists. This is also
+ possible with MRs
+ - Changed padding & background color for highlighted notes
+ - Re-add the newrelic_rpm gem which was removed without any deprecation or warning (Stan Hu)
+ - Update sentry-raven gem to 0.15.6
+ - Add build coverage in project's builds page (Steffen Köhler)
+ - Changed # to ! for merge requests in activity view
+
+v 8.5.0
+ - Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
+ - Cache various Repository methods to improve performance (Yorick Peterse)
+ - Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu)
+ - Ensure rake tasks that don't need a DB connection can be run without one
+ - Update New Relic gem to 3.14.1.311 (Stan Hu)
+ - Add "visibility" flag to GET /projects api endpoint
+ - Add an option to supply root email through an environmental variable (Koichiro Mikami)
+ - Ignore binary files in code search to prevent Error 500 (Stan Hu)
+ - Render sanitized SVG images (Stan Hu)
+ - Support download access by PRIVATE-TOKEN header (Stan Hu)
+ - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push
+ - Add option to include the sender name in body of Notify email (Jason Lee)
+ - New UI for pagination
+ - Don't prevent sign out when 2FA enforcement is enabled and user hasn't yet
+ set it up
+ - API: Added "merge_requests/:merge_request_id/closes_issues" (Gal Schlezinger)
+ - Fix diff comments loaded by AJAX to load comment with diff in discussion tab
+ - Fix relative links in other markup formats (Ben Boeckel)
+ - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel)
+ - Fix label links for a merge request pointing to issues list
+ - Don't vendor minified JS
+ - Increase project import timeout to 15 minutes
+ - Be more permissive with email address validation: it only has to contain a single '@'
+ - Display 404 error on group not found
+ - Track project import failure
+ - Support Two-factor Authentication for LDAP users
+ - Display database type and version in Administration dashboard
+ - Allow limited Markdown in Broadcast Messages
+ - Fix visibility level text in admin area (Zeger-Jan van de Weg)
+ - Warn admin during OAuth of granting admin rights (Zeger-Jan van de Weg)
+ - Update the ExternalIssue regex pattern (Blake Hitchcock)
+ - Remember user's inline/side-by-side diff view preference in a cookie (Kirill Katsnelson)
+ - Optimized performance of finding issues to be closed by a merge request
+ - Add `avatar_url`, `description`, `git_ssh_url`, `git_http_url`, `path_with_namespace`
+ and `default_branch` in `project` in push, issue, merge-request and note webhooks data (Kirill Zaitsev)
+ - Deprecate the `ssh_url` in favor of `git_ssh_url` and `http_url` in favor of `git_http_url`
+ in `project` for push, issue, merge-request and note webhooks data (Kirill Zaitsev)
+ - Deprecate the `repository` key in push, issue, merge-request and note webhooks data, use `project` instead (Kirill Zaitsev)
+ - API: Expose MergeRequest#merge_status (Andrei Dziahel)
+ - Revert "Add IP check against DNSBLs at account sign-up"
+ - Actually use the `skip_merges` option in Repository#commits (Tony Chu)
+ - Fix API to keep request parameters in Link header (Michael Potthoff)
+ - Deprecate API "merge_request/:merge_request_id/comments". Use "merge_requests/:merge_request_id/notes" instead
+ - Deprecate API "merge_request/:merge_request_id/...". Use "merge_requests/:merge_request_id/..." instead
+ - Prevent parse error when name of project ends with .atom and prevent path issues
+ - Discover branches for commit statuses ref-less when doing merge when succeeded
+ - Mark inline difference between old and new paths when a file is renamed
+ - Support Akismet spam checking for creation of issues via API (Stan Hu)
+ - API: Allow to set or update a merge-request's milestone (Kirill Skachkov)
+ - Improve UI consistency between projects and groups lists
+ - Add sort dropdown to dashboard projects page
+ - Fixed logo animation on Safari (Roman Rott)
+ - Fix Merge When Succeeded when multiple stages
+ - Hide remove source branch button when the MR is merged but new commits are pushed (Zeger-Jan van de Weg)
+ - In seach autocomplete show only groups and projects you are member of
+ - Don't process cross-reference notes from forks
+ - Fix: init.d script not working on OS X
+ - Faster snippet search
+ - Added API to download build artifacts
+ - Title for milestones should be unique (Zeger-Jan van de Weg)
+ - Validate correctness of maximum attachment size application setting
+ - Replaces "Create merge request" link with one to the "Merge Request" when one exists
+ - Fix CI builds badge, add a new link to builds badge, deprecate the old one
+ - Fix broken link to project in build notification emails
+ - Ability to see and sort on vote count from Issues and MR lists
+ - Fix builds scheduler when first build in stage was allowed to fail
+ - User project limit is reached notice is hidden if the projects limit is zero
+ - Add API support for managing runners and project's runners
+ - Allow SAML users to login with no previous account without having to allow
+ all Omniauth providers to do so.
+ - Allow existing users to auto link their SAML credentials by logging in via SAML
+ - Make it possible to erase a build (trace, artifacts) using UI and API
+ - Ability to revert changes from a Merge Request or Commit
+ - Emoji comment on diffs are not award emoji
+ - Add label description (Nuttanart Pornprasitsakul)
+ - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
+ - Add Todos
+
+v 8.4.5
+ - No CE-specific changes
+
+v 8.4.4
+ - Update omniauth-saml gem to 1.4.2
+ - Prevent long-running backup tasks from timing out the database connection
+ - Add a Project setting to allow guests to view build logs (defaults to true)
+ - Sort project milestones by due date including issue editor (Oliver Rogers / Orih)
+
+v 8.4.3
+ - Increase lfs_objects size column to 8-byte integer to allow files larger
+ than 2.1GB
+ - Correctly highlight MR diff when MR has merge conflicts
+ - Fix highlighting in blame view
+ - Update sentry-raven gem to prevent "Not a git repository" console output
+ when running certain commands
+ - Add instrumentation to additional Gitlab::Git and Rugged methods for
+ performance monitoring
+ - Allow autosize textareas to also be manually resized
+
+v 8.4.2
+ - Bump required gitlab-workhorse version to bring in a fix for missing
+ artifacts in the build artifacts browser
+ - Get rid of those ugly borders on the file tree view
+ - Fix updating the runner information when asking for builds
+ - Bump gitlab_git version to 7.2.24 in order to bring in a performance
+ improvement when checking if a repository was empty
+ - Add instrumentation for Gitlab::Git::Repository instance methods so we can
+ track them in Performance Monitoring.
+ - Increase contrast between highlighted code comments and inline diff marker
+ - Fix method undefined when using external commit status in builds
+ - Fix highlighting in blame view.
+
+v 8.4.1
+ - Apply security updates for Rails (4.2.5.1), rails-html-sanitizer (1.0.3),
+ and Nokogiri (1.6.7.2)
+ - Fix redirect loop during import
+ - Fix diff highlighting for all syntax themes
+ - Delete project and associations in a background worker
+
+v 8.4.0
+ - Allow LDAP users to change their email if it was not set by the LDAP server
+ - Ensure Gravatar host looks like an actual host
+ - Consider re-assign as a mention from a notification point of view
+ - Add pagination headers to already paginated API resources
+ - Properly generate diff of orphan commits, like the first commit in a repository
+ - Improve the consistency of commit titles, branch names, tag names, issue/MR titles, on their respective project pages
+ - Autocomplete data is now always loaded, instead of when focusing a comment text area
+ - Improved performance of finding issues for an entire group
+ - Added custom application performance measuring system powered by InfluxDB
+ - Add syntax highlighting to diffs
+ - Gracefully handle invalid UTF-8 sequences in Markdown links (Stan Hu)
+ - Bump fog to 1.36.0 (Stan Hu)
+ - Add user's last used IP addresses to admin page (Stan Hu)
+ - Add housekeeping function to project settings page
+ - The default GitLab logo now acts as a loading indicator
+ - Fix caching issue where build status was not updating in project dashboard (Stan Hu)
+ - Accept 2xx status codes for successful Webhook triggers (Stan Hu)
+ - Fix missing date of month in network graph when commits span a month (Stan Hu)
- Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu)
+ - Don't notify users twice if they are both project watchers and subscribers (Stan Hu)
+ - Remove gray background from layout in UI
+ - Fix signup for OAuth providers that don't provide a name
- Implement new UI for group page
- Implement search inside emoji picker
+ - Let the CI runner know about builds that this build depends on
- Add API support for looking up a user by username (Stan Hu)
- Add project permissions to all project API endpoints (Stan Hu)
+ - Link to milestone in "Milestone changed" system note
- Only allow group/project members to mention `@all`
- Expose Git's version in the admin area (Trey Davis)
- Add "Frequently used" category to emoji picker
- Add CAS support (tduehr)
- Add link to merge request on build detail page
+ - Fix: Problem with projects ending with .keys (Jose Corcuera)
- Revert back upvote and downvote button to the issue and MR pages
- Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg)
+ - Add system hook messages for project rename and transfer (Steve Norman)
- Fix version check image in Safari
-
-v 8.3.3 (unreleased)
+ - Show 'All' tab by default in the builds page
+ - Add Open Graph and Twitter Card data to all pages
+ - Fix API project lookups when querying with a namespace with dots (Stan Hu)
+ - Enable forcing Two-factor authentication sitewide, with optional grace period
+ - Import GitHub Pull Requests into GitLab
+ - Change single user API endpoint to return more detailed data (Michael Potthoff)
+ - Update version check images to use SVG
+ - Validate README format before displaying
+ - Enable Microsoft Azure OAuth2 support (Janis Meybohm)
+ - Properly set task-list class on single item task lists
+ - Add file finder feature in tree view (Kyungchul Shin)
+ - Ajax filter by message for commits page
+ - API: Add support for deleting a tag via the API (Robert Schilling)
+ - Allow subsequent validations in CI Linter
+ - Show referenced MRs & Issues only when the current viewer can access them
+ - Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee)
+ - Add API support for managing project's builds
+ - Add API support for managing project's build triggers
+ - Add API support for managing project's build variables
+ - Allow broadcast messages to be edited
+ - Autosize Markdown textareas
+ - Import GitHub wiki into GitLab
+ - Add reporters ability to download and browse build artifacts (Andrew Johnson)
+ - Autofill referring url in message box when reporting user abuse.
+ - Remove leading comma on award emoji when the user is the first to award the emoji (Zeger-Jan van de Weg)
+ - Add build artifacts browser
+ - Improve UX in builds artifacts browser
+ - Increase default size of `data` column in `events` table when using MySQL
+ - Expose button to CI Lint tool on project builds page
+ - Fix: Creator should be added as a master of the project on creation
+ - Added X-GitLab-... headers to emails from CI and Email On Push services (Anton Baklanov)
+ - Add IP check against DNSBLs at account sign-up
+ - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
+
+v 8.3.4
+ - Use gitlab-workhorse 0.5.4 (fixes API routing bug)
+
+v 8.3.3
+ - Preserve CE behavior with JIRA integration by only calling API if URL is set
+ - Fix duplicated branch creation/deletion events when using Web UI (Stan Hu)
+ - Add configurable LDAP server query timeout
+ - Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running
+ - Suppress e-mails on failed builds if allow_failure is set (Stan Hu)
- Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu)
+ - Better support for referencing and closing issues in Asana service (Mike Wyatt)
- Enable "Add key" button when user fills in a proper key (Stan Hu)
+ - Fix error in processing reply-by-email messages (Jason Lee)
+ - Fix Error 500 when visiting build page of project with nil runners_token (Stan Hu)
+ - Use WOFF versions of SourceSansPro fonts
+ - Fix regression when builds were not generated for tags created through web/api interface
+ - Fix: maintain milestone filter between Open and Closed tabs (Greg Smethells)
+ - Fix missing artifacts and build traces for build created before 8.3
v 8.3.2
- Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
@@ -28,7 +334,6 @@ v 8.3.1
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
- Fix LDAP identity and user retrieval when special characters are used
- Move Sidekiq-cron configuration to gitlab.yml
- - Enable forcing Two-Factor authentication sitewide, with optional grace period
v 8.3.0
- Bump rack-attack to 4.3.1 for security fix (Stan Hu)
@@ -36,6 +341,7 @@ v 8.3.0
- Add open_issues_count to project API (Stan Hu)
- Expand character set of usernames created by Omniauth (Corey Hinshaw)
- Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
+ - Add unsubscribe link in the email footer (Zeger-Jan van de Weg)
- Provide better diagnostic message upon project creation errors (Stan Hu)
- Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu)
- Remove api credentials from link to build_page
@@ -44,11 +350,13 @@ v 8.3.0
- Fix broken group avatar upload under "New group" (Stan Hu)
- Update project repositorize size and commit count during import:repos task (Stan Hu)
- Fix API setting of 'public' attribute to false will make a project private (Stan Hu)
- - Handle and report SSL errors in Web hook test (Stan Hu)
+ - Handle and report SSL errors in Webhook test (Stan Hu)
- Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu)
- Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
+ - WIP identifier on merge requests no longer requires trailing space
- Add rake tasks for git repository maintainance (Zeger-Jan van de Weg)
- Fix 500 error when update group member permission
+ - Fix: As an admin, cannot add oneself as a member to a group/project
- Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
- Recognize issue/MR/snippet/commit links as references
- Backport JIRA features from EE to CE
@@ -92,6 +400,8 @@ v 8.3.0
- Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present
- Persist runners registration token in database
- Fix online editor should not remove newlines at the end of the file
+ - Expose Git's version in the admin area
+ - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
v 8.2.3
- Fix application settings cache not expiring after changes (Stan Hu)
@@ -108,7 +418,6 @@ v 8.2.2
- Fix Error 500 when viewing user's personal projects from admin page (Stan Hu)
- Fix: Raw private snippets access workflow
- Prevent "413 Request entity too large" errors when pushing large files with LFS
- - Fix: As an admin, cannot add oneself as a member to a group/project
- Fix invalid links within projects dashboard header
- Make current user the first user in assignee dropdown in issues detail page (Stan Hu)
- Fix: duplicate email notifications on issue comments
@@ -150,6 +459,8 @@ v 8.2.0
- Allow to define cache in `.gitlab-ci.yml`
- Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
- Remove deprecated CI events from project settings page
+ - Use issue editor as cross reference comment author when issue is edited with a new mention.
+ - Add graphs of commits ahead and behind default branch (Jeff Stubler)
- Improve personal snippet access workflow (Douglas Alexandre)
- [API] Add ability to fetch the commit ID of the last commit that actually touched a file
- Fix omniauth documentation setting for omnibus configuration (Jon Cairns)
@@ -225,6 +536,7 @@ v 8.1.0
- Improved performance of the trending projects page
- Remove CI migration task
- Improved performance of finding projects by their namespace
+ - Add assignee data to Issuables' hook_data (Bram Daams)
- Fix bug where transferring a project would result in stale commit links (Stan Hu)
- Fix build trace updating
- Include full path of source and target branch names in New Merge Request page (Stan Hu)
@@ -258,7 +570,7 @@ v 8.1.0
- Ensure code blocks are properly highlighted after a note is updated
- Fix wrong access level badge on MR comments
- Hide password in the service settings form
- - Move CI web hooks page to project settings area
+ - Move CI webhooks page to project settings area
- Fix User Identities API. It now allows you to properly create or update user's identities.
- Add user preference to change layout width (Peter Göbel)
- Use commit status in merge request widget as preferred source of CI status
@@ -301,7 +613,7 @@ v 8.0.3
- Fix URL shown in Slack notifications
- Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
- Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
- - Add work_in_progress key to MR web hooks (Ben Boeckel)
+ - Add work_in_progress key to MR webhooks (Ben Boeckel)
v 8.0.2
- Fix default avatar not rendering in network graph (Stan Hu)
@@ -592,7 +904,7 @@ v 7.12.0
- Fix milestone "Browse Issues" button.
- Set milestone on new issue when creating issue from index with milestone filter active.
- Make namespace API available to all users (Stan Hu)
- - Add web hook support for note events (Stan Hu)
+ - Add webhook support for note events (Stan Hu)
- Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu)
- Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu)
- Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu)
@@ -699,7 +1011,7 @@ v 7.11.0
- Add "Create Merge Request" buttons to commits and branches pages and push event.
- Show user roles by comments.
- Fix automatic blocking of auto-created users from Active Directory.
- - Call merge request web hook for each new commits (Arthur Gautier)
+ - Call merge request webhook for each new commits (Arthur Gautier)
- Use SIGKILL by default in Sidekiq::MemoryKiller
- Fix mentioning of private groups.
- Add style for <kbd> element in markdown
@@ -873,7 +1185,7 @@ v 7.9.0
- Add brakeman (security scanner for Ruby on Rails)
- Slack username and channel options
- Add grouped milestones from all projects to dashboard.
- - Web hook sends pusher email as well as commiter
+ - Webhook sends pusher email as well as commiter
- Add Bitbucket omniauth provider.
- Add Bitbucket importer.
- Support referencing issues to a project whose name starts with a digit
@@ -996,7 +1308,7 @@ v 7.8.0
- Allow notification email to be set separately from primary email.
- API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
- Don't have Markdown preview fail for long comments/wiki pages.
- - When test web hook - show error message instead of 500 error page if connection to hook url was reset
+ - When test webhook - show error message instead of 500 error page if connection to hook url was reset
- Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
- Added persistent collapse button for left side nav bar (Jason Blanchard)
- Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
@@ -1013,7 +1325,7 @@ v 7.8.0
- Show projects user contributed to on user page. Show stars near project on user page.
- Improve database performance for GitLab
- Add Asana service (Jeremy Benoist)
- - Improve project web hooks with extra data
+ - Improve project webhooks with extra data
v 7.7.2
- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
@@ -1498,7 +1810,7 @@ v 6.4.0
- Side-by-side diff view (Steven Thonus)
- Internal projects (Jason Hollingsworth)
- Allow removal of avatar (Drew Blessing)
- - Project web hooks now support issues and merge request events
+ - Project webhooks now support issues and merge request events
- Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
- Expire event cache on avatar creation/removal (Drew Blessing)
- Archiving old projects (Steven Thonus)
@@ -1568,7 +1880,7 @@ v 6.2.0
- Added search for projects by name to api (Izaak Alpert)
- Make default user theme configurable (Izaak Alpert)
- Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev)
- - Rake tasks for web hooks management (Jonhnny Weslley)
+ - Rake tasks for webhooks management (Jonhnny Weslley)
- Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov)
- API: Remove group
- API: Remove project
@@ -1771,7 +2083,7 @@ v 4.2.0
- Async gitolite calls
- added satellites logs
- can_create_group, can_create_team booleans for User
- - Process web hooks async
+ - Process webhooks async
- GFM: Fix images escaped inside links
- Network graph improved
- Switchable branches for network graph
@@ -1805,7 +2117,7 @@ v 4.1.0
v 4.0.0
- Remove project code and path from API. Use id instead
- - Return valid cloneable url to repo for web hook
+ - Return valid cloneable url to repo for webhook
- Fixed backup issue
- Reorganized settings
- Fixed commits compare
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b9c2b3d2f8e..7540fa1afcc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,3 +1,32 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [Contribute to GitLab](#contribute-to-gitlab)
+ - [Contributor license agreement](#contributor-license-agreement)
+ - [Security vulnerability disclosure](#security-vulnerability-disclosure)
+ - [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
+ - [Helping others](#helping-others)
+ - [I want to contribute!](#i-want-to-contribute)
+ - [Implement design & UI elements](#implement-design-ui-elements)
+ - [Design reference](#design-reference)
+ - [UI development kit](#ui-development-kit)
+ - [Issue tracker](#issue-tracker)
+ - [Feature proposals](#feature-proposals)
+ - [Issue tracker guidelines](#issue-tracker-guidelines)
+ - [Issue weight](#issue-weight)
+ - [Regression issues](#regression-issues)
+ - [Merge requests](#merge-requests)
+ - [Merge request guidelines](#merge-request-guidelines)
+ - [Merge request description format](#merge-request-description-format)
+ - [Contribution acceptance criteria](#contribution-acceptance-criteria)
+ - [Changes for Stable Releases](#changes-for-stable-releases)
+ - [Definition of done](#definition-of-done)
+ - [Style guides](#style-guides)
+ - [Code of conduct](#code-of-conduct)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
# Contribute to GitLab
Thank you for your interest in contributing to GitLab. This guide details how
@@ -8,7 +37,7 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for
abbreviation.
-If you have read this guide and want to know how the GitLab [core-team][]
+If you have read this guide and want to know how the GitLab [core team][core-team]
operates please see [the GitLab contributing process](PROCESS.md).
## Contributor license agreement
@@ -42,10 +71,10 @@ for audiences of all ages.
## Helping others
Please help other GitLab users when you can. The channels people will reach out
-on can be found on the [getting help page][].
+on can be found on the [getting help page][getting-help].
Sign up for the mailing list, answer GitLab questions on StackOverflow or
-respond in the IRC channel. You can also sign up on [CodeTriage][] to help with
+respond in the IRC channel. You can also sign up on [CodeTriage][codetriage] to help with
the remaining issues on the GitHub issue tracker.
## I want to contribute!
@@ -57,6 +86,22 @@ GitLab.
This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
+## Implement design & UI elements
+
+### Design reference
+
+The GitLab design reference can be found in the [gitlab-design] project.
+The designs are made using Antetype (`.atype` files). You can use the
+[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
+(the PNG is 1:1).
+
+The current designs can be found in the [`gitlab1.atype` file].
+
+### UI development kit
+
+Implemented UI elements can also be found at https://gitlab.com/help/ui. Please
+note that this page isn't comprehensive at this time.
+
## Issue tracker
To get support for your particular problem please use the
@@ -89,7 +134,7 @@ For feature proposals for EE, open an issue on the
In order to help track the feature proposals, we have created a
[`feature proposal`][fpl] label. For the time being, users that are not members
-of the project cannot add labels. You can instead ask one of the [core team][]
+of the project cannot add labels. You can instead ask one of the [core team][core-team]
members to add the label `feature proposal` to the issue.
Please keep feature proposals as small and simple as possible, complex ones
@@ -147,7 +192,7 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true)
sudo gitlab-rake gitlab:env:info)
(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:env:info)
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production)
## Possible fixes
@@ -177,6 +222,26 @@ is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9.
issues or chunks. You can simply not set the weight of a parent issue and set
weights to children issues.
+### Regression issues
+
+Every monthly release has a corresponding issue on the CE issue tracker to keep
+track of functionality broken by that release and any fixes that need to be
+included in a patch release (see [8.3 Regressions] as an example).
+
+As outlined in the issue description, the intended workflow is to post one note
+with a reference to an issue describing the regression, and then to update that
+note with a reference to the merge request that fixes it as it becomes available.
+
+If you're a contributor who doesn't have the required permissions to update
+other users' notes, please post a new note with a reference to both the issue
+and the merge request.
+
+The release manager will [update the notes] in the regression issue as fixes are
+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
+
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
@@ -214,15 +279,17 @@ request is as follows:
1. Add your changes to the [CHANGELOG](CHANGELOG)
1. If you are changing the README, some documentation or other things which
have no effect on the tests, add `[ci skip]` somewhere in the commit message
+ and make sure to read the [documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by
[squashing them][git-squash]
1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the master branch
1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you
- used to achieve it
+ used to achieve it, see the [merge request description format]
+ (#merge-request-description-format)
1. If the MR changes the UI it should include before and after screenshots
-1. If the MR changes CSS classes please include the list of affected pages
+1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R`
1. Link any relevant [issues][ce-tracker] in the merge request description and
leave a comment on them with a link back to the MR
@@ -251,51 +318,35 @@ to us than having a minimal commit log. The smaller an MR is the more likely it
is it will be merged (quickly). After that you can send more MRs to enhance it.
For examples of feedback on merge requests please look at already
-[closed merge requests][]. If you would like quick feedback on your merge
-request feel free to mention one of the Merge Marshalls of the [core team][].
+[closed merge requests][closed-merge-requests]. If you would like quick feedback
+on your merge request feel free to mention one of the Merge Marshalls in the
+[core team][core-team] or one of the
+[Merge request coaches](https://about.gitlab.com/team/).
Please ensure that your merge request meets the contribution acceptance criteria.
-## Definition of done
+When having your code reviewed and when reviewing merge requests please take the
+[Thoughtbot code review guide] into account.
-If you contribute to GitLab please know that changes involve more than just
-code. We have the following [definition of done][]. Please ensure you support
-the feature you contribute through all of these steps.
+### Merge request description format
-1. Description explaining the relevancy (see following item)
-1. Working and clean code that is commented where needed
-1. Unit and integration tests that pass on the CI server
-1. Documented in the /doc directory
-1. Changelog entry added
-1. Reviewed and any concerns are addressed
-1. Merged by the project lead
-1. Added to the release blog article
-1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/) if relevant
-1. Community questions answered
-1. Answers to questions radiated (in docs/wiki/etc.)
+Please submit merge requests using the following template in the merge request
+description area. Copy-paste it to retain the markdown format.
-If you add a dependency in GitLab (such as an operating system package) please
-consider updating the following and note the applicability of each in your
-merge request:
+```
+## What does this MR do?
-1. Note the addition in the release blog post (create one if it doesn't exist yet) https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/
-1. Upgrade guide, for example https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md
-1. Upgrader https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md#2-run-gitlab-upgrade-tool
-1. Installation guide https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies
-1. GitLab Development Kit https://gitlab.com/gitlab-org/gitlab-development-kit
-1. Test suite https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh
-1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab
+## Are there points in the code the reviewer needs to double check?
-## Merge request description format
+## Why was this MR needed?
-1. What does this MR do?
-1. Are there points in the code the reviewer needs to double check?
-1. Why was this MR needed?
-1. What are the relevant issue numbers?
-1. Screenshots (if relevant)
+## What are the relevant issue numbers?
-## Contribution acceptance criteria
+## Screenshots (if relevant)
+```
+
+### Contribution acceptance criteria
-1. The change is as small as possible (see the above paragraph for details)
+1. The change is as small as possible
1. Include proper tests and make all tests pass (unless it contains a test
exposing a bug in existing code)
1. If you suspect a failing CI build is unrelated to your contribution, you may
@@ -308,20 +359,64 @@ merge request:
1. Does not break any existing functionality
1. Fixes one specific issue or implements one specific feature (do not combine
things, send separate merge requests if needed)
-1. Migrations should do only one thing (eg: either create a table, move data to
- a new table or remove an old table) to aid retrying on failure
+1. Migrations should do only one thing (e.g., either create a table, move data
+ to a new table or remove an old table) to aid retrying on failure
1. Keeps the GitLab code base clean and well structured
1. Contains functionality we think other users will benefit from too
-1. Doesn't add configuration options since they complicate future changes
+1. Doesn't add configuration options or settings options since they complicate
+ making and testing future changes
1. Changes after submitting the merge request should be in separate commits
(no squashing). If necessary, you will be asked to squash when the review is
over, before merging.
-1. It conforms to the following style guides:
- * If your change touches a line that does not follow the style, modify the
+1. It conforms to the [style guides](#style-guides) and the following:
+ - If your change touches a line that does not follow the style, modify the
entire line to follow it. This prevents linting tools from generating warnings.
- * Don't touch neighbouring lines. As an exception, automatic mass
+ - Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant.
+## Changes for Stable Releases
+
+Sometimes certain changes have to be added to an existing stable release.
+Two examples are bug fixes and performance improvements. In these cases the
+corresponding merge request should be updated to have the following:
+
+1. A milestone indicating what release the merge request should be merged into.
+1. The label "Pick into Stable"
+
+This makes it easier for release managers to keep track of what still has to be
+merged and where changes have to be merged into.
+Like all merge requests the target should be master so all bugfixes are in master.
+
+## Definition of done
+
+If you contribute to GitLab please know that changes involve more than just
+code. We have the following [definition of done][definition-of-done]. Please ensure you support
+the feature you contribute through all of these steps.
+
+1. Description explaining the relevancy (see following item)
+1. Working and clean code that is commented where needed
+1. Unit and integration tests that pass on the CI server
+1. [Documented][doc-styleguide] in the /doc directory
+1. Changelog entry added
+1. Reviewed and any concerns are addressed
+1. Merged by the project lead
+1. Added to the release blog article
+1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/) if relevant
+1. Community questions answered
+1. Answers to questions radiated (in docs/wiki/etc.)
+
+If you add a dependency in GitLab (such as an operating system package) please
+consider updating the following and note the applicability of each in your
+merge request:
+
+1. Note the addition in the release blog post (create one if it doesn't exist yet) https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/
+1. Upgrade guide, for example https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md
+1. Upgrader https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md#2-run-gitlab-upgrade-tool
+1. Installation guide https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies
+1. GitLab Development Kit https://gitlab.com/gitlab-org/gitlab-development-kit
+1. Test suite https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh
+1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab
+
## Style guides
1. [Ruby](https://github.com/bbatsov/ruby-style-guide).
@@ -332,11 +427,12 @@ merge request:
1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing)
1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
+1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security
-1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
1. [Database Migrations](doc/development/migration_style_guide.md)
-1. [Documentation styleguide](doc_styleguide.md)
+1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
+1. [Documentation styleguide][doc-styleguide]
1. Interface text should be written subjectively instead of objectively. It
should be the GitLab core team addressing a person. It should be written in
present time and never use past tense (has been/was). For example instead
@@ -374,12 +470,12 @@ when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior can be
reported by emailing `contact@gitlab.com`.
-This Code of Conduct is adapted from the [Contributor Covenant][], version 1.1.0,
+This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
-[core team]: https://about.gitlab.com/core-team/
-[getting help page]: https://about.gitlab.com/getting-help/
-[Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
+[core-team]: https://about.gitlab.com/core-team/
+[getting-help]: https://about.gitlab.com/getting-help/
+[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs
[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
@@ -393,8 +489,14 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
-[closed merge requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
-[definition of done]: http://guide.agilealliance.org/guide/definition-of-done.html
-[Contributor Covenant]: http://contributor-covenant.org
+[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
+[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
+[contributor-covenant]: http://contributor-covenant.org
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
+[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
+[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
+[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
+[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
+[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/
+[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index d48d3702aed..bc02b8685c1 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.9
+2.6.11
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 4b9fcbec101..ef5e4454454 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.5.1
+0.6.5
diff --git a/Gemfile b/Gemfile
index 2a1c4f7d73a..e500bfb7885 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,14 +1,14 @@
source "https://rubygems.org"
-gem 'rails', '4.2.4'
+gem 'rails', '4.2.5.2'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
-# Specify a sprockets version due to security issue
-# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
-gem 'sprockets', '~> 2.12.3'
+# Specify a sprockets version due to increased performance
+# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
+gem 'sprockets', '~> 3.3.5'
# Default values for AR models
gem "default_value_for", "~> 3.0.0"
@@ -18,10 +18,12 @@ gem "mysql2", '~> 0.3.16', group: :mysql
gem "pg", '~> 0.18.2', group: :postgres
# Authentication libraries
-gem 'devise', '~> 3.5.3'
+gem 'devise', '~> 3.5.4'
gem 'devise-async', '~> 0.9.0'
gem 'doorkeeper', '~> 2.2.0'
-gem 'omniauth', '~> 1.2.2'
+gem 'omniauth', '~> 1.3.1'
+gem 'omniauth-auth0', '~> 1.4.1'
+gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 3.0.0'
@@ -29,14 +31,15 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
-gem 'omniauth-saml', '~> 1.4.0'
+gem 'omniauth-saml', '~> 1.5.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
-gem 'omniauth_crowd'
+gem 'omniauth_crowd', '~> 2.2.0'
gem 'rack-oauth2', '~> 1.2.1'
-# reCAPTCHA protection
+# Spam and anti-bot protection
gem 'recaptcha', require: 'recaptcha/rails'
+gem 'akismet', '~> 2.0'
# Two-factor authentication
gem 'devise-two-factor', '~> 2.0.0'
@@ -48,7 +51,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 7.2.20'
+gem "gitlab_git", '~> 10.0'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -56,7 +59,9 @@ gem "gitlab_git", '~> 7.2.20'
gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
# Git Wiki
-gem 'gollum-lib', '~> 4.1.0'
+# Required manually in config/initializers/gollum.rb to control load order
+gem 'gollum-lib', '~> 4.1.0', require: false
+gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
# Language detection
gem "github-linguist", "~> 4.7.0", require: "linguist"
@@ -66,10 +71,6 @@ gem 'grape', '~> 0.13.0'
gem 'grape-entity', '~> 0.4.2'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
-# Format dates and times
-# based on human-friendly examples
-gem "stamp", '~> 0.6.0'
-
# Pagination
gem "kaminari", "~> 0.16.3"
@@ -77,13 +78,13 @@ gem "kaminari", "~> 0.16.3"
gem "haml-rails", '~> 0.9.0'
# Files attachments
-gem "carrierwave", '~> 0.9.0'
+gem "carrierwave", '~> 0.10.0'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
# for aws storage
-gem "fog", "~> 1.25.0"
+gem "fog", "~> 1.36.0"
gem "unf", '~> 0.1.4'
# Authorization
@@ -106,14 +107,15 @@ gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 1.10.1'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
-gem 'nokogiri', '1.6.7.1'
+# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
+gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2'
# Diffs
gem 'diffy', '~> 3.0.3'
# Application server
group :unicorn do
- gem "unicorn", '~> 4.8.2'
+ gem "unicorn", '~> 4.9.0'
gem 'unicorn-worker-killer', '~> 0.4.2'
end
@@ -169,10 +171,10 @@ gem 'asana', '~> 0.4.0'
gem 'ruby-fogbugz', '~> 0.2.1'
# d3
-gem 'd3_rails', '~> 3.5.5'
+gem 'd3_rails', '~> 3.5.0'
#cal-heatmap
-gem "cal-heatmap-rails", "~> 0.0.1"
+gem 'cal-heatmap-rails', '~> 3.5.0'
# underscore-rails
gem "underscore-rails", "~> 1.8.0"
@@ -181,6 +183,9 @@ gem "underscore-rails", "~> 1.8.0"
gem "sanitize", '~> 2.0'
gem 'babosa', '~> 1.0.2'
+# Sanitizes SVG input
+gem "loofah", "~> 2.0.3"
+
# Protect against bruteforcing
gem "rack-attack", '~> 4.3.1'
@@ -200,21 +205,23 @@ gem 'turbolinks', '~> 2.5.0'
gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
-gem 'bootstrap-sass', '~> 3.0'
+gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2'
-gem 'gitlab_emoji', '~> 0.2.0'
+gem 'gitlab_emoji', '~> 0.3.0'
gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.0.0'
gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 5.0.0'
-gem 'nprogress-rails', '~> 0.1.6.7'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
+# Sentry integration
+gem 'sentry-raven', '~> 0.15'
+
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
@@ -250,13 +257,15 @@ group :development, :test do
gem 'byebug', platform: :mri
gem 'pry-rails'
- gem 'awesome_print', '~> 1.2.0'
+ gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0'
- gem 'database_cleaner', '~> 1.4.0'
- gem 'factory_girl_rails', '~> 4.3.0'
- gem 'rspec-rails', '~> 3.3.0'
- gem 'spinach-rails', '~> 0.2.1'
+ gem 'database_cleaner', '~> 1.4.0'
+ gem 'factory_girl_rails', '~> 4.6.0'
+ gem 'rspec-rails', '~> 3.3.0'
+ gem 'rspec-retry'
+ gem 'spinach-rails', '~> 0.2.1'
+ gem 'spinach-rerun-reporter', '~> 0.0.2'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
@@ -264,19 +273,20 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.0.0'
- gem 'capybara', '~> 2.4.0'
+ gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
- gem 'poltergeist', '~> 1.8.1'
+ gem 'poltergeist', '~> 1.9.0'
gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine', '~> 2.2.0'
- gem 'spring', '~> 1.3.6'
+ gem 'spring', '~> 1.6.4'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.35.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
gem 'flog', require: false
@@ -298,10 +308,9 @@ group :production do
gem "gitlab_meta", '7.0'
end
-gem "newrelic_rpm", '~> 3.9.4.245'
-gem 'newrelic-grape'
+gem "newrelic_rpm", '~> 3.14'
-gem 'octokit', '~> 3.7.0'
+gem 'octokit', '~> 3.8.0'
gem "mail_room", "~> 0.6.1"
diff --git a/Gemfile.lock b/Gemfile.lock
index 9769ae80a7d..63ed9441c62 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,41 +4,41 @@ GEM
CFPropertyList (2.3.2)
RedCloth (4.2.9)
ace-rails-ap (2.0.1)
- actionmailer (4.2.4)
- actionpack (= 4.2.4)
- actionview (= 4.2.4)
- activejob (= 4.2.4)
+ actionmailer (4.2.5.2)
+ actionpack (= 4.2.5.2)
+ actionview (= 4.2.5.2)
+ activejob (= 4.2.5.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (4.2.4)
- actionview (= 4.2.4)
- activesupport (= 4.2.4)
+ actionpack (4.2.5.2)
+ actionview (= 4.2.5.2)
+ activesupport (= 4.2.5.2)
rack (~> 1.6)
rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (4.2.4)
- activesupport (= 4.2.4)
+ actionview (4.2.5.2)
+ activesupport (= 4.2.5.2)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (4.2.4)
- activesupport (= 4.2.4)
+ activejob (4.2.5.2)
+ activesupport (= 4.2.5.2)
globalid (>= 0.3.0)
- activemodel (4.2.4)
- activesupport (= 4.2.4)
+ activemodel (4.2.5.2)
+ activesupport (= 4.2.5.2)
builder (~> 3.1)
- activerecord (4.2.4)
- activemodel (= 4.2.4)
- activesupport (= 4.2.4)
+ activerecord (4.2.5.2)
+ activemodel (= 4.2.5.2)
+ activesupport (= 4.2.5.2)
arel (~> 6.0)
activerecord-deprecated_finders (1.0.4)
activerecord-session_store (0.1.2)
actionpack (>= 4.0.0, < 5)
activerecord (>= 4.0.0, < 5)
railties (>= 4.0.0, < 5)
- activesupport (4.2.4)
+ activesupport (4.2.5.2)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
@@ -49,7 +49,8 @@ GEM
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
- allocations (1.0.3)
+ akismet (2.0.0)
+ allocations (1.0.4)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
@@ -66,7 +67,7 @@ GEM
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
- autoprefixer-rails (6.1.2)
+ autoprefixer-rails (6.2.3)
execjs
json
awesome_print (1.2.0)
@@ -82,9 +83,9 @@ GEM
erubis (>= 2.6.6)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootstrap-sass (3.3.5)
- autoprefixer-rails (>= 5.0.0.1)
- sass (>= 3.2.19)
+ bootstrap-sass (3.3.6)
+ autoprefixer-rails (>= 5.2.1)
+ sass (>= 3.3.4)
brakeman (3.1.4)
erubis (~> 2.6)
fastercsv (~> 1.5)
@@ -106,8 +107,9 @@ GEM
bundler (~> 1.2)
thor (~> 0.18)
byebug (8.2.1)
- cal-heatmap-rails (0.0.1)
- capybara (2.4.4)
+ cal-heatmap-rails (3.5.1)
+ capybara (2.6.2)
+ addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
@@ -116,10 +118,11 @@ GEM
capybara-screenshot (1.0.11)
capybara (>= 1.0, < 3)
launchy
- carrierwave (0.9.0)
+ carrierwave (0.10.0)
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
+ mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.3)
chunky_png (1.3.5)
@@ -157,7 +160,7 @@ GEM
activerecord (>= 3.2.0, < 5.0)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- devise (3.5.3)
+ devise (3.5.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 3.2.6, < 5)
@@ -193,10 +196,10 @@ GEM
excon (0.45.4)
execjs (2.6.0)
expression_parser (0.9.0)
- factory_girl (4.3.0)
+ factory_girl (4.5.0)
activesupport (>= 3.0.0)
- factory_girl_rails (4.3.0)
- factory_girl (~> 4.3.0)
+ factory_girl_rails (4.6.0)
+ factory_girl (~> 4.5.0)
railties (>= 3.0.0)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
@@ -219,21 +222,45 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
- fog (1.25.0)
+ fog (1.36.0)
+ fog-aliyun (>= 0.1.0)
+ fog-atmos
+ fog-aws (>= 0.6.0)
fog-brightbox (~> 0.4)
- fog-core (~> 1.25)
+ fog-core (~> 1.32)
+ fog-dynect (~> 0.0.2)
+ fog-ecloud (~> 0.1)
+ fog-google (<= 0.1.0)
fog-json
+ fog-local
+ fog-powerdns (>= 0.1.1)
fog-profitbricks
fog-radosgw (>= 0.0.2)
+ fog-riakcs
fog-sakuracloud (>= 0.0.4)
+ fog-serverlove
fog-softlayer
+ fog-storm_on_demand
fog-terremark
fog-vmfusion
fog-voxel
+ fog-xenserver
fog-xml (~> 0.1.1)
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
- opennebula
+ fog-aliyun (0.1.0)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ ipaddress (~> 0.8)
+ xml-simple (~> 1.1)
+ fog-atmos (0.1.0)
+ fog-core
+ fog-xml
+ fog-aws (0.8.1)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ fog-xml (~> 0.1)
+ ipaddress (~> 0.8)
fog-brightbox (0.10.1)
fog-core (~> 1.22)
fog-json
@@ -242,21 +269,48 @@ GEM
builder
excon (~> 0.45)
formatador (~> 0.2)
+ fog-dynect (0.0.2)
+ fog-core
+ fog-json
+ fog-xml
+ fog-ecloud (0.3.0)
+ fog-core
+ fog-xml
+ fog-google (0.1.0)
+ fog-core
+ fog-json
+ fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
+ fog-local (0.2.1)
+ fog-core (~> 1.27)
+ fog-powerdns (0.1.1)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ fog-xml (~> 0.1)
fog-profitbricks (0.0.5)
fog-core
fog-xml
nokogiri
- fog-radosgw (0.0.4)
+ fog-radosgw (0.0.5)
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
- fog-sakuracloud (1.5.0)
+ fog-riakcs (0.1.0)
+ fog-core
+ fog-json
+ fog-xml
+ fog-sakuracloud (1.7.5)
+ fog-core
+ fog-json
+ fog-serverlove (0.1.2)
fog-core
fog-json
- fog-softlayer (1.0.2)
+ fog-softlayer (1.0.3)
+ fog-core
+ fog-json
+ fog-storm_on_demand (0.1.1)
fog-core
fog-json
fog-terremark (0.1.0)
@@ -268,6 +322,9 @@ GEM
fog-voxel (0.1.0)
fog-core
fog-xml
+ fog-xenserver (0.2.2)
+ fog-core
+ fog-xml
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
@@ -281,11 +338,11 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
- gemojione (2.1.1)
+ gemojione (2.2.1)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
- github-linguist (4.7.3)
+ github-linguist (4.7.5)
charlock_holmes (~> 0.7.3)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
@@ -300,13 +357,13 @@ GEM
diff-lcs (~> 1.1)
mime-types (~> 1.15)
posix-spawn (~> 0.3)
- gitlab_emoji (0.2.0)
- gemojione (~> 2.1)
- gitlab_git (7.2.22)
+ gitlab_emoji (0.3.1)
+ gemojione (~> 2.2, >= 2.2.1)
+ gitlab_git (10.0.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
- rugged (~> 0.23.3)
+ rugged (~> 0.24.0)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
@@ -324,6 +381,9 @@ GEM
rouge (~> 1.9)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
+ gollum-rugged_adapter (0.4.2)
+ mime-types (>= 1.15)
+ rugged (~> 0.24.0, >= 0.21.3)
gon (6.0.1)
actionpack (>= 3.0)
json
@@ -352,7 +412,6 @@ GEM
railties (>= 4.0.1)
hashie (3.4.3)
highline (1.7.8)
- hike (1.2.3)
hipchat (1.5.2)
httparty
mimemagic
@@ -377,7 +436,7 @@ GEM
influxdb (0.2.3)
cause
json
- ipaddress (0.8.0)
+ ipaddress (0.8.2)
jquery-atwho-rails (1.3.2)
jquery-rails (4.0.5)
rails-dom-testing (~> 1.0)
@@ -424,13 +483,9 @@ GEM
net-ldap (0.12.1)
net-ssh (3.0.1)
netrc (0.11.0)
- newrelic-grape (2.1.0)
- grape
- newrelic_rpm
- newrelic_rpm (3.9.4.245)
- nokogiri (1.6.7.1)
+ newrelic_rpm (3.14.1.311)
+ nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
- nprogress-rails (0.1.6.7)
oauth (0.4.7)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
@@ -438,11 +493,17 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (~> 1.2)
- octokit (3.7.1)
+ octokit (3.8.0)
sawyer (~> 0.6.0, >= 0.5.3)
- omniauth (1.2.2)
+ omniauth (1.3.1)
hashie (>= 1.2, < 4)
- rack (~> 1.0)
+ rack (>= 1.0, < 3)
+ omniauth-auth0 (1.4.1)
+ omniauth-oauth2 (~> 1.1)
+ omniauth-azure-oauth2 (0.0.6)
+ jwt (~> 1.0)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.1)
omniauth-bitbucket (0.0.2)
multi_json (~> 1.7)
omniauth (~> 1.1)
@@ -476,9 +537,9 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
- omniauth-saml (1.4.1)
- omniauth (~> 1.1)
- ruby-saml (~> 1.0.0)
+ omniauth-saml (1.5.0)
+ omniauth (~> 1.3)
+ ruby-saml (~> 1.1, >= 1.1.1)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
omniauth-twitter (1.2.1)
@@ -488,10 +549,6 @@ GEM
activesupport
nokogiri (>= 1.4.4)
omniauth (~> 1.0)
- opennebula (4.14.2)
- json
- nokogiri
- rbvmomi
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
@@ -500,7 +557,7 @@ GEM
parser (2.2.3.0)
ast (>= 1.1, < 3.0)
pg (0.18.4)
- poltergeist (1.8.1)
+ poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
@@ -534,16 +591,16 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.2.4)
- actionmailer (= 4.2.4)
- actionpack (= 4.2.4)
- actionview (= 4.2.4)
- activejob (= 4.2.4)
- activemodel (= 4.2.4)
- activerecord (= 4.2.4)
- activesupport (= 4.2.4)
+ rails (4.2.5.2)
+ actionmailer (= 4.2.5.2)
+ actionpack (= 4.2.5.2)
+ actionview (= 4.2.5.2)
+ activejob (= 4.2.5.2)
+ activemodel (= 4.2.5.2)
+ activerecord (= 4.2.5.2)
+ activesupport (= 4.2.5.2)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.2.4)
+ railties (= 4.2.5.2)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
@@ -551,26 +608,22 @@ GEM
activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6.0)
rails-deprecated_sanitizer (>= 1.0.1)
- rails-html-sanitizer (1.0.2)
+ rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- railties (4.2.4)
- actionpack (= 4.2.4)
- activesupport (= 4.2.4)
+ railties (4.2.5.2)
+ actionpack (= 4.2.5.2)
+ activesupport (= 4.2.5.2)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.0.0)
raindrops (0.15.0)
- rake (10.4.2)
+ rake (10.5.0)
raphael-rails (2.1.2)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
- rbvmomi (1.8.2)
- builder
- nokogiri (>= 1.4.1)
- trollop
rdoc (3.12.2)
json (~> 1.4)
recaptcha (1.0.2)
@@ -598,8 +651,8 @@ GEM
request_store (1.2.1)
rerun (0.11.0)
listen (~> 3.0)
- responders (2.1.0)
- railties (>= 4.2.0, < 5)
+ responders (2.1.1)
+ railties (>= 4.2.0, < 5.1)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
@@ -631,6 +684,8 @@ GEM
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
+ rspec-retry (0.4.5)
+ rspec-core
rspec-support (3.3.0)
rubocop (0.35.1)
astrolabe (~> 1.3)
@@ -642,7 +697,7 @@ GEM
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-progressbar (1.7.5)
- ruby-saml (1.0.0)
+ ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
ruby2ruby (2.2.0)
@@ -653,7 +708,7 @@ GEM
rubyntlm (0.5.2)
rubypants (0.2.0)
rufus-scheduler (3.1.10)
- rugged (0.23.3)
+ rugged (0.24.0)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
@@ -667,6 +722,9 @@ GEM
sawyer (0.6.0)
addressable (~> 2.3.5)
faraday (~> 0.8, < 0.10)
+ scss_lint (0.47.1)
+ rake (>= 0.9, < 11)
+ sass (~> 3.4.15)
sdoc (0.3.20)
json (>= 1.1.3)
rdoc (~> 3.10)
@@ -675,6 +733,8 @@ GEM
activesupport (>= 3.1, < 4.3)
select2-rails (3.5.9.3)
thor (~> 0.14)
+ sentry-raven (0.15.6)
+ faraday (>= 0.7.6)
settingslogic (2.0.9)
sexp_processor (4.6.0)
sham_rack (1.3.6)
@@ -714,23 +774,21 @@ GEM
capybara (>= 2.0.0)
railties (>= 3)
spinach (>= 0.4)
- spring (1.3.6)
+ spinach-rerun-reporter (0.0.2)
+ spinach (~> 0.8)
+ spring (1.6.4)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-spinach (1.0.0)
spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1)
- sprockets (2.12.4)
- hike (~> 1.2)
- multi_json (~> 1.0)
- rack (~> 1.0)
- tilt (~> 1.1, != 1.3.0)
+ sprockets (3.3.5)
+ rack (> 1, < 3)
sprockets-rails (2.3.3)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
- stamp (0.6.0)
state_machines (0.4.0)
state_machines-activemodel (0.3.0)
activemodel (~> 4.1)
@@ -758,7 +816,7 @@ GEM
rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.5)
- tilt (1.4.1)
+ tilt (2.0.2)
timfel-krb5-auth (0.8.3)
tinder (1.10.1)
eventmachine (~> 1.0)
@@ -770,7 +828,6 @@ GEM
multi_json (~> 1.7)
twitter-stream (~> 0.1)
tins (1.6.0)
- trollop (2.1.2)
turbolinks (2.5.3)
coffee-rails
twitter-stream (0.1.16)
@@ -786,7 +843,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.1)
- unicorn (4.8.3)
+ unicorn (4.9.0)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
@@ -819,6 +876,7 @@ GEM
builder
expression_parser
rinku
+ xml-simple (1.1.5)
xpath (2.0.0)
nokogiri (~> 1.3)
@@ -833,6 +891,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
after_commit_queue
+ akismet (~> 2.0)
allocations (~> 1.0)
annotate (~> 2.6.0)
asana (~> 0.4.0)
@@ -843,26 +902,26 @@ DEPENDENCIES
benchmark-ips
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
- bootstrap-sass (~> 3.0)
+ bootstrap-sass (~> 3.3.0)
brakeman (~> 3.1.0)
browser (~> 1.0.0)
bullet
bundler-audit
byebug
- cal-heatmap-rails (~> 0.0.1)
- capybara (~> 2.4.0)
+ cal-heatmap-rails (~> 3.5.0)
+ capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
- carrierwave (~> 0.9.0)
+ carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
creole (~> 0.5.0)
- d3_rails (~> 3.5.5)
+ d3_rails (~> 3.5.0)
database_cleaner (~> 1.4.0)
default_value_for (~> 3.0.0)
- devise (~> 3.5.3)
+ devise (~> 3.5.4)
devise-async (~> 0.9.0)
devise-two-factor (~> 2.0.0)
diffy (~> 3.0.3)
@@ -870,11 +929,11 @@ DEPENDENCIES
dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0)
- factory_girl_rails (~> 4.3.0)
+ factory_girl_rails (~> 4.6.0)
ffaker (~> 2.0.0)
flay
flog
- fog (~> 1.25.0)
+ fog (~> 1.36.0)
font-awesome-rails (~> 4.2)
foreman
fuubar (~> 2.0.0)
@@ -882,11 +941,12 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_emoji (~> 0.2.0)
- gitlab_git (~> 7.2.20)
+ gitlab_emoji (~> 0.3.0)
+ gitlab_git (~> 10.0)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
+ gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.0.1)
grape (~> 0.13.0)
grape-entity (~> 0.4.2)
@@ -902,6 +962,7 @@ DEPENDENCIES
jquery-ui-rails (~> 5.0.0)
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
+ loofah (~> 2.0.3)
mail_room (~> 0.6.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
@@ -909,13 +970,13 @@ DEPENDENCIES
mysql2 (~> 0.3.16)
nested_form (~> 0.3.2)
net-ssh (~> 3.0.1)
- newrelic-grape
- newrelic_rpm (~> 3.9.4.245)
- nokogiri (= 1.6.7.1)
- nprogress-rails (~> 0.1.6.7)
+ newrelic_rpm (~> 3.14)
+ nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.0.0)
- octokit (~> 3.7.0)
- omniauth (~> 1.2.2)
+ octokit (~> 3.8.0)
+ omniauth (~> 1.3.1)
+ omniauth-auth0 (~> 1.4.1)
+ omniauth-azure-oauth2 (~> 0.0.6)
omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 3.0.0)
@@ -923,20 +984,20 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0)
- omniauth-saml (~> 1.4.0)
+ omniauth-saml (~> 1.5.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
- omniauth_crowd
+ omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
paranoia (~> 2.0)
pg (~> 0.18.2)
- poltergeist (~> 1.8.1)
+ poltergeist (~> 1.9.0)
pry-rails
quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.1)
rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1)
- rails (= 4.2.4)
+ rails (= 4.2.5.2)
rails-deprecated_sanitizer (~> 1.0.3)
raphael-rails (~> 2.1.2)
rblineprof
@@ -951,13 +1012,16 @@ DEPENDENCIES
rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0)
+ rspec-retry
rubocop (~> 0.35.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.0)
+ scss_lint (~> 0.47.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
+ sentry-raven (~> 0.15)
settingslogic (~> 2.0.9)
sham_rack
shoulda-matchers (~> 2.8.0)
@@ -968,12 +1032,12 @@ DEPENDENCIES
six (~> 0.2.0)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
- spring (~> 1.3.6)
+ spinach-rerun-reporter (~> 0.0.2)
+ spring (~> 1.6.4)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0)
spring-commands-teaspoon (~> 0.0.2)
- sprockets (~> 2.12.3)
- stamp (~> 0.6.0)
+ sprockets (~> 3.3.5)
state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2)
teaspoon (~> 1.0.0)
@@ -985,7 +1049,7 @@ DEPENDENCIES
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
- unicorn (~> 4.8.2)
+ unicorn (~> 4.9.0)
unicorn-worker-killer (~> 0.4.2)
version_sorter (~> 2.0.0)
virtus (~> 1.0.1)
@@ -994,4 +1058,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.10.6
+ 1.11.2
diff --git a/PROCESS.md b/PROCESS.md
index 5f4d67bc10e..cad45d23df9 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -2,23 +2,39 @@
## Purpose of describing the contributing process
-Below we describe the contributing process to GitLab for two reasons. So that contributors know what to expect from maintainers (possible responses, friendly treatment, etc.). And so that maintainers know what to expect from contributors (use the latest version, ensure that the issue is addressed, friendly treatment, etc.).
+Below we describe the contributing process to GitLab for two reasons. So that
+contributors know what to expect from maintainers (possible responses, friendly
+treatment, etc.). And so that maintainers know what to expect from contributors
+(use the latest version, ensure that the issue is addressed, friendly treatment,
+etc.).
## Common actions
### Issue team
-- Looks for issues without [workflow labels](#how-we-handle-issues) and triages issue
-- Closes invalid issues with a comment (duplicates, [fixed in newer version](#issue-fixed-in-newer-version), [issue report for old version](#issue-report-for-old-version), not a problem in GitLab, etc.)
-- Asks for feedback from issue reporter ([invalid issue reports](#improperly-formatted-issue), [format code](#code-format), etc.)
-- Monitors all issues for feedback (but especially ones commented on since automatically watching them)
+
+- Looks for issues without [workflow labels](#how-we-handle-issues) and triages
+ issue
+- Closes invalid issues with a comment (duplicates,
+ [fixed in newer version](#issue-fixed-in-newer-version),
+ [issue report for old version](#issue-report-for-old-version), not a problem
+ in GitLab, etc.)
+- Asks for feedback from issue reporter
+ ([invalid issue reports](#improperly-formatted-issue),
+ [format code](#code-format), etc.)
+- Monitors all issues for feedback (but especially ones commented on since
+ automatically watching them)
- Closes issues with no feedback from the reporter for two weeks
-### Merge marshal
+### Merge marshall & merge request coach
-- Responds to merge requests the issue team mentions them in and monitors for new merge requests
-- Provides feedback to the merge request submitter to improve the merge request (style, tests, etc.)
-- Mark merge requests 'ready-for-merge' when they meet the contribution guidelines
-- Mention developer(s) based on the [list of members and their specialities](https://about.gitlab.com/core-team/)
+- Responds to merge requests the issue team mentions them in and monitors for
+ new merge requests
+- Provides feedback to the merge request submitter to improve the merge request
+ (style, tests, etc.)
+- Mark merge requests `Ready for Merge` when they meet the
+ [contribution acceptance criteria]
+- Mention developer(s) based on the
+ [list of members and their specialities][team]
- Closes merge requests with no feedback from the reporter for two weeks
## Priorities of the issue team
@@ -30,29 +46,40 @@ Below we describe the contributing process to GitLab for two reasons. So that co
## Mentioning people
-The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help on those issue. Please select someone with relevant experience from [GitLab core team](https://about.gitlab.com/core-team/). If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a timely response. If the involvement of the lead developer is needed the other core team members will mention this person.
+The most important thing is making sure valid issues receive feedback from the
+development team. Therefore the priority is mentioning developers that can help
+on those issue. Please select someone with relevant experience from
+[GitLab core team][core-team]. If there is nobody mentioned with that expertise
+look in the commit history for the affected files to find someone. Avoid
+mentioning the lead developer, this is the person that is least likely to give a
+timely response. If the involvement of the lead developer is needed the other
+core team members will mention this person.
## Workflow labels
-Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue.
-
-- *Awaiting feedback*: Feedback pending from the reporter
-- *Awaiting confirmation of fix*: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away)
-- *Attached MR*: There is a MR attached and the discussion should happen there
- - We need to let issues stay in sync with the MR's. We can do this with a "Closing #XXXX" or "Fixes #XXXX" comment in the MR. We can't close the issue when there is a merge request because sometimes a MR is not good and we just close the MR, then the issue must stay.
-- *Developer*: needs help from a developer
-- *UX* needs needs help from a UX designer
-- *Frontend* needs help from a Front-end engineer
-- *Graphics* needs help from a Graphics designer
-- *up-for-grabs* is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels.
-- *feature proposal* is a proposal for a new feature for GitLab. People are encouraged to vote
+Workflow labels are purposely not very detailed since that would be hard to keep
+updated as you would need to re-evaluate them after every comment. We optionally
+use functional labels on demand when want to group related issues to get an
+overview (for example all issues related to RVM, to tackle them in one go) and
+to add details to the issue.
+
+- ~"Awaiting Feedback" Feedback pending from the reporter
+- ~UX needs help from a UX designer
+- ~Frontend needs help from a Front-end engineer. Please follow the
+ ["Implement design & UI elements" guidelines].
+- ~up-for-grabs is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels.
+- ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote
in support or comment for further detail. Do not use `feature request`.
-
+- ~bug is an issue reporting undesirable or incorrect behavior.
+- ~customer is an issue reported by enterprise subscribers. This label should
+be accompanied by *bug* or *feature proposal* labels.
Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label.
## Functional labels
-These labels describe what development specialities are involved such as: PostgreSQL, UX, LDAP.
+These labels describe what development specialities are involved such as: `CI`,
+`Core`, `Documentation`, `Frontend`, `Issues`, `Merge Requests`, `Omnibus`,
+`Release`, `Repository`, `UX`.
## Assigning issues
@@ -60,21 +87,29 @@ If an issue is complex and needs the attention of a specific person, assignment
## Label colors
-- Light orange `#fef2c0`: workflow labels for issue team members (awaiting feedback, awaiting confirmation of fix)
-- Bright orange `#eb6420`: workflow labels for core team members (attached MR, awaiting developer action/feedback)
-- Light blue `#82C5FF`: functional labels
-- Green labels `#009800`: issues that can generally be ignored. For example, issues given the following labels normally can be closed immediately:
- - Support (see copy & paste response: [Support requests and configuration questions](#support-requests-and-configuration-questions)
+- Light orange `#fef2c0`: workflow labels for issue team members (awaiting
+ feedback, awaiting confirmation of fix)
+- Bright orange `#eb6420`: workflow labels for core team members (attached MR,
+ awaiting developer action/feedback)
+- Light blue `#82C5FF`: functional labels
+- Green labels `#009800`: issues that can generally be ignored. For example,
+ issues given the following labels normally can be closed immediately:
+ - Support (see copy & paste response:
+ [Support requests and configuration questions](#support-requests-and-configuration-questions)
## Be kind
-Be kind to people trying to contribute. Be aware that people may be a non-native English speaker, they might not understand things or they might be very sensitive as to how you word things. Use Emoji to express your feelings (heart, star, smile, etc.). Some good tips about giving feedback to merge requests is in the [Thoughtbot code review guide](https://github.com/thoughtbot/guides/tree/master/code-review).
+Be kind to people trying to contribute. Be aware that people may be a non-native
+English speaker, they might not understand things or they might be very
+sensitive as to how you word things. Use Emoji to express your feelings (heart,
+star, smile, etc.). Some good tips about giving feedback to merge requests is in
+the [Thoughtbot code review guide].
## Copy & paste responses
### Improperly formatted issue
-Thanks for the issue report. Please reformat your issue to conform to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report. Please reformat your issue to conform to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Issue report for old version
@@ -110,11 +145,11 @@ This merge request has been closed because a request for more information has no
### Accepting merge requests
-Is there an issue on the [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues)
-that is similar to this?
-Could you please link it here?
+Is there an issue on the
+\[issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
+similar to this? Could you please link it here?
Please be aware that new functionality that is not marked
-[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
+\[accepting merge requests\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
might not make it into GitLab.
### Only accepting merge requests with green tests
@@ -129,4 +164,10 @@ rebase with master to see if that solves the issue.
We are currently in the process of closing down the issue tracker on GitHub, to
prevent duplication with the GitLab.com issue tracker.
Since this is an older issue I'll be closing this for now. If you think this is
-still an issue I encourage you to open it on the \[GitLab.com issue tracker\](https://gitlab.com/gitlab-org/gitlab-ce/issues).
+still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues).
+
+[core-team]: https://about.gitlab.com/core-team/
+[team]: https://about.gitlab.com/team/
+[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
+["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
+[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
diff --git a/Procfile b/Procfile
index 9cfdee7040f..cad738d4292 100644
--- a/Procfile
+++ b/Procfile
@@ -2,6 +2,6 @@
# https://gitlab.com/gitlab-org/omnibus-gitlab or the init scripts in
# lib/support/init.d, which call scripts in bin/ .
#
-web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
-worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default
+web: RAILS_ENV=development bin/web start_foreground
+worker: RAILS_ENV=development bin/background_jobs start_foreground
# mail_room: bundle exec mail_room -q -c config/mail_room.yml
diff --git a/README.md b/README.md
index 3ec1d4a776c..afa60116ebb 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.1
-- Git 1.7.10+
+- Git 2.7.4+
- Redis 2.8+
- MySQL or PostgreSQL
diff --git a/Rakefile b/Rakefile
index 35b2f05cbb4..5dd389d5678 100644..100755
--- a/Rakefile
+++ b/Rakefile
@@ -4,4 +4,7 @@
require File.expand_path('../config/application', __FILE__)
+relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__)
+require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
+
Gitlab::Application.load_tasks
diff --git a/VERSION b/VERSION
index ce669730119..cac7d91adda 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.4.0.pre
+8.6.0-pre
diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt
index a9b845ed1d4..df187637e18 100755..100644
--- a/app/assets/fonts/OFL.txt
+++ b/app/assets/fonts/OFL.txt
@@ -1,7 +1,8 @@
-Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
+
+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf
deleted file mode 100644
index 9c9b5cb7f03..00000000000
--- a/app/assets/fonts/SourceSansPro-Black.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff
new file mode 100644
index 00000000000..b7e86200927
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Black.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2
new file mode 100644
index 00000000000..c90d078406c
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Black.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf b/app/assets/fonts/SourceSansPro-BlackIt.ttf
deleted file mode 100644
index 294ce5abe8f..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
new file mode 100644
index 00000000000..c3314b1ef06
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2
new file mode 100644
index 00000000000..b87e22c41b5
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackItalic.ttf b/app/assets/fonts/SourceSansPro-BlackItalic.ttf
deleted file mode 100755
index c719243c0d6..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf
deleted file mode 100644
index 5d65c93242f..00000000000
--- a/app/assets/fonts/SourceSansPro-Bold.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff
new file mode 100644
index 00000000000..d1d40f840f8
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2
new file mode 100644
index 00000000000..0f46f3e833a
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf b/app/assets/fonts/SourceSansPro-BoldIt.ttf
deleted file mode 100644
index 3decd130070..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
new file mode 100644
index 00000000000..ef6ff514d3a
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2
new file mode 100644
index 00000000000..8007df6df32
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldItalic.ttf b/app/assets/fonts/SourceSansPro-BoldItalic.ttf
deleted file mode 100755
index d20dd0c5eca..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
deleted file mode 100644
index 253eafa3783..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
new file mode 100644
index 00000000000..1e6c94d9eb3
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2
new file mode 100644
index 00000000000..b715f274082
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
deleted file mode 100644
index 00d7e9a7aa8..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
new file mode 100644
index 00000000000..7a408b1ec73
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2
new file mode 100644
index 00000000000..d8f9d29d4aa
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf b/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
deleted file mode 100755
index 2c34f3b8dc4..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf
deleted file mode 100644
index f7af5377595..00000000000
--- a/app/assets/fonts/SourceSansPro-It.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff
new file mode 100644
index 00000000000..4d54bc95718
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-It.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2
new file mode 100644
index 00000000000..a00852641f8
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-It.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Italic.ttf b/app/assets/fonts/SourceSansPro-Italic.ttf
deleted file mode 100755
index e5a1a86e631..00000000000
--- a/app/assets/fonts/SourceSansPro-Italic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf
deleted file mode 100644
index 83a0a336661..00000000000
--- a/app/assets/fonts/SourceSansPro-Light.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff
new file mode 100644
index 00000000000..1706d57d3c5
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Light.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2
new file mode 100644
index 00000000000..d8b610ad76e
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Light.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf
deleted file mode 100644
index f18827985ef..00000000000
--- a/app/assets/fonts/SourceSansPro-LightIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
new file mode 100644
index 00000000000..87378d6c609
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2
new file mode 100644
index 00000000000..e0eebac8273
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightItalic.ttf b/app/assets/fonts/SourceSansPro-LightItalic.ttf
deleted file mode 100755
index 88a6778d24f..00000000000
--- a/app/assets/fonts/SourceSansPro-LightItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf
deleted file mode 100644
index 44486cdc670..00000000000
--- a/app/assets/fonts/SourceSansPro-Regular.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff
new file mode 100644
index 00000000000..460ab12a638
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2
new file mode 100644
index 00000000000..0dd3464c74b
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf
deleted file mode 100644
index 86b00c067e0..00000000000
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
new file mode 100644
index 00000000000..43379631b2d
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2
new file mode 100644
index 00000000000..2526d2e1b60
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
deleted file mode 100644
index 13d66a1fc45..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
new file mode 100644
index 00000000000..232c2048ae7
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2
new file mode 100644
index 00000000000..606935af089
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf b/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf
deleted file mode 100755
index 2c5ad3008c3..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png
new file mode 100644
index 00000000000..a82c751e001
--- /dev/null
+++ b/app/assets/images/auth_buttons/azure_64.png
Binary files differ
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index a8ad7b6eab6..1e7cf79ea45 100644
--- a/app/assets/images/emoji.png
+++ b/app/assets/images/emoji.png
Binary files differ
diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png
new file mode 100644
index 00000000000..74d67f7520d
--- /dev/null
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
index 63803747413..5092e824e65 100644
--- a/app/assets/javascripts/activities.js.coffee
+++ b/app/assets/javascripts/activities.js.coffee
@@ -1,7 +1,7 @@
class @Activities
constructor: ->
Pager.init 20, true
- $(".event-filter .btn").bind "click", (event) =>
+ $(".event-filter-link").on "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
@@ -12,18 +12,10 @@ class @Activities
toggleFilter: (sender) ->
- sender.toggleClass "active"
+ $('.event-filter .active').removeClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
- if event_filters
- event_filters = event_filters.split(",")
- else
- event_filters = new Array()
+ $.cookie "event_filter", (if event_filters isnt filter then filter else ""), { path: '/' }
- index = event_filters.indexOf(filter)
- if index is -1
- event_filters.push filter
- else
- event_filters.splice index, 1
-
- $.cookie "event_filter", event_filters.join(","), { path: '/' }
+ if event_filters isnt filter
+ sender.closest('li').toggleClass "active"
diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee
index bcb2e6df7c0..b2b8e1b7ffb 100644
--- a/app/assets/javascripts/admin.js.coffee
+++ b/app/assets/javascripts/admin.js.coffee
@@ -10,20 +10,7 @@ class @Admin
$('body').on 'click', '.js-toggle-colors-link', (e) ->
e.preventDefault()
- $('.js-toggle-colors-link').hide()
- $('.js-toggle-colors-container').show()
-
- $('input#broadcast_message_color').on 'input', ->
- previewColor = $('input#broadcast_message_color').val()
- $('div.broadcast-message-preview').css('background-color', previewColor)
-
- $('input#broadcast_message_font').on 'input', ->
- previewColor = $('input#broadcast_message_font').val()
- $('div.broadcast-message-preview').css('color', previewColor)
-
- $('textarea#broadcast_message_message').on 'input', ->
- previewMessage = $('textarea#broadcast_message_message').val()
- $('div.broadcast-message-preview span').text(previewMessage)
+ $('.js-toggle-colors-container').toggle()
$('.log-tabs a').click (e) ->
e.preventDefault()
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 746fa3cea87..2ddf8612db3 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -4,6 +4,7 @@
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
+ labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -47,7 +48,7 @@
callback(namespaces)
# Return projects list. Filtered by query
- projects: (query, callback) ->
+ projects: (query, order, callback) ->
url = Api.buildUrl(Api.projects_path)
$.ajax(
@@ -55,11 +56,25 @@
data:
private_token: gon.api_token
search: query
+ order_by: order
per_page: 20
dataType: "json"
).done (projects) ->
callback(projects)
+ newLabel: (project_id, data, callback) ->
+ url = Api.buildUrl(Api.labels_path)
+ url = url.replace(':id', project_id)
+
+ data.private_token = gon.api_token
+ $.ajax(
+ url: url
+ type: "POST"
+ data: data
+ dataType: "json"
+ ).done (label) ->
+ callback(label)
+
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index affab5bb030..d415bbd3476 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -5,32 +5,32 @@
# the compiled file.
#
#= require jquery
-#= require jquery-ui
+#= require jquery-ui/autocomplete
+#= require jquery-ui/datepicker
+#= require jquery-ui/effect-highlight
+#= require jquery-ui/sortable
#= require jquery_ujs
#= require jquery.cookie
#= require jquery.endless-scroll
#= require jquery.highlight
-#= require jquery.history
#= require jquery.waitforimages
#= require jquery.atwho
#= require jquery.scrollTo
-#= require jquery.blockUI
#= require jquery.turbolinks
+#= require d3
+#= require cal-heatmap
#= require turbolinks
#= require autosave
#= require bootstrap
#= require select2
#= require raphael
-#= require g.raphael-min
-#= require g.bar-min
-#= require chart-lib.min
+#= require g.raphael
+#= require g.bar
+#= require Chart
#= require branch-graph
#= require ace/ace
#= require ace/ext-searchbox
-#= require d3
#= require underscore
-#= require nprogress
-#= require nprogress-turbolinks
#= require dropzone
#= require mousetrap
#= require mousetrap/pause
@@ -39,9 +39,9 @@
#= require shortcuts_dashboard_navigation
#= require shortcuts_issuable
#= require shortcuts_network
-#= require cal-heatmap
-#= require jquery.nicescroll.min
+#= require jquery.nicescroll
#= require_tree .
+#= require fuzzaldrin-plus
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -107,6 +107,8 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
+
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
@@ -204,4 +206,72 @@ $ ->
form = btn.closest("form")
new ConfirmDangerModal(form, text)
+ $('input[type="search"]').each ->
+ $this = $(this)
+ $this.attr 'value', $this.val()
+ return
+
+ $(document)
+ .off 'keyup', 'input[type="search"]'
+ .on 'keyup', 'input[type="search"]' , (e) ->
+ $this = $(this)
+ $this.attr 'value', $this.val()
+
+ $(document)
+ .off 'breakpoint:change'
+ .on 'breakpoint:change', (e, breakpoint) ->
+ if breakpoint is 'sm' or breakpoint is 'xs'
+ $gutterIcon = $('.js-sidebar-toggle').find('i')
+ if $gutterIcon.hasClass('fa-angle-double-right')
+ $gutterIcon.closest('a').trigger('click')
+
+ $(document)
+ .off 'click', '.js-sidebar-toggle'
+ .on 'click', '.js-sidebar-toggle', (e, triggered) ->
+ e.preventDefault()
+ $this = $(this)
+ $thisIcon = $this.find 'i'
+ $allGutterToggleIcons = $('.js-sidebar-toggle i')
+ if $thisIcon.hasClass('fa-angle-double-right')
+ $allGutterToggleIcons
+ .removeClass('fa-angle-double-right')
+ .addClass('fa-angle-double-left')
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed')
+ $('.page-with-sidebar')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed')
+ else
+ $allGutterToggleIcons
+ .removeClass('fa-angle-double-left')
+ .addClass('fa-angle-double-right')
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded')
+ $('.page-with-sidebar')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded')
+ if not triggered
+ $.cookie("collapsed_gutter",
+ $('.right-sidebar')
+ .hasClass('right-sidebar-collapsed'), { path: '/' })
+
+ fitSidebarForSize = ->
+ oldBootstrapBreakpoint = bootstrapBreakpoint
+ bootstrapBreakpoint = bp.getBreakpointSize()
+ if bootstrapBreakpoint != oldBootstrapBreakpoint
+ $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+
+ checkInitialSidebarSize = ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
+ if bootstrapBreakpoint is "xs" or "sm"
+ $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+
+ $(window)
+ .off "resize"
+ .on "resize", (e) ->
+ fitSidebarForSize()
+
+ checkInitialSidebarSize()
new Aside()
diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee
index 5d3fe81da74..28f8e103664 100644
--- a/app/assets/javascripts/autosave.js.coffee
+++ b/app/assets/javascripts/autosave.js.coffee
@@ -16,11 +16,11 @@ class @Autosave
try
text = window.localStorage.getItem @key
- catch
+ catch e
return
@field.val text if text?.length > 0
- @field.trigger "input"
+ @field.trigger "input"
save: ->
return unless window.localStorage?
@@ -35,5 +35,5 @@ class @Autosave
reset: ->
return unless window.localStorage?
- try
+ try
window.localStorage.removeItem @key
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 619abb1fb07..03a44874161 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,25 +1,55 @@
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
- $(".add-award").click (event)->
+ $(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
- $(".emoji-menu").show()
- $("html").click ->
+ @showEmojiMenu()
+
+ $("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
- $(".emoji-menu").hide()
+ $(".emoji-menu").removeClass "is-visible"
+
+ $(".awards")
+ .off "click"
+ .on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock()
- @setupSearch()
+
+ handleClick: (e) ->
+ e.preventDefault()
+ emoji = $(this)
+ .find(".icon")
+ .data "emoji"
+ awards_handler.addAward emoji
+
+ showEmojiMenu: ->
+ if $(".emoji-menu").length
+ if $(".emoji-menu").is ".is-visible"
+ $(".emoji-menu").removeClass "is-visible"
+ $("#emoji_search").blur()
+ else
+ $(".emoji-menu").addClass "is-visible"
+ $("#emoji_search").focus()
+ else
+ $('.js-add-award').addClass "is-loading"
+ $.get "/emojis", (response) =>
+ $('.js-add-award').removeClass "is-loading"
+ $(".js-award-holder").append response
+ setTimeout =>
+ $(".emoji-menu").addClass "is-visible"
+ $("#emoji_search").focus()
+ @setupSearch()
+ , 200
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
- $(".emoji-menu").hide()
-
+ $(".emoji-menu").removeClass "is-visible"
+
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
@@ -28,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji)
@decrementCounter(emoji)
else
- counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
@@ -42,31 +72,38 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
- counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent()
-
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
emojiIcon.removeClass("active")
@removeMeFromAuthorList(emoji)
- else if emoji =="thumbsup" || emoji == "thumbsdown"
+ else if emoji == "thumbsup" || emoji == "thumbsdown"
emojiIcon.tooltip("destroy")
counter.text(0)
emojiIcon.removeClass("active")
+ @removeMeFromAuthorList(emoji)
else
emojiIcon.tooltip("destroy")
emojiIcon.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
- authors = award_block.attr("data-original-title").split(", ")
- authors = _.without(authors, "me").join(", ")
- award_block.attr("title", authors)
+ authors = award_block
+ .attr("data-original-title")
+ .split(", ")
+ authors.splice(authors.indexOf("me"),1)
+ award_block
+ .closest(".js-emoji-btn")
+ .attr("data-original-title", authors.join(", "))
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
- authors = award_block.attr("data-original-title").split(", ")
+ origTitle = award_block.attr("data-original-title").trim()
+ authors = []
+ if origTitle
+ authors = origTitle.split(', ')
authors.push("me")
award_block.attr("title", authors.join(", "))
@resetTooltip(award_block)
@@ -78,20 +115,24 @@ class @AwardsHandler
setTimeout (->
award.tooltip()
), 200
-
+
createEmoji: (emoji) ->
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
- nodes.push("<div class='award active' title='me'>")
- nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
- nodes.push("<div class='counter'>1</div>")
- nodes.push("</div>")
-
- emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
-
- $(".award").tooltip()
+ nodes.push(
+ "<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>"
+ )
+
+ emoji_node = $(nodes.join("\n"))
+ .insertBefore(".js-award-holder")
+ .find(".emoji-icon")
+ .data("emoji", emoji)
+ $('.award-control').tooltip()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
@@ -114,7 +155,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
- $(".award [data-emoji='#{emoji}']")
+ $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
@@ -150,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val()
# Clean previous search results
- $("ul.emoji-search,h5.emoji-search").remove()
+ $("ul.emoji-menu-search, h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show()
- ul = $("<ul>").addClass("emoji-search").append(found_emojis)
+ ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul)
else
diff --git a/app/assets/javascripts/behaviors/autosize.js.coffee b/app/assets/javascripts/behaviors/autosize.js.coffee
new file mode 100644
index 00000000000..a072fe48a98
--- /dev/null
+++ b/app/assets/javascripts/behaviors/autosize.js.coffee
@@ -0,0 +1,22 @@
+#= require jquery.ba-resize
+#= require autosize
+
+$ ->
+ $fields = $('.js-autosize')
+
+ $fields.on 'autosize:resized', ->
+ $field = $(@)
+ $field.data('height', $field.outerHeight())
+
+ $fields.on 'resize.autosize', ->
+ $field = $(@)
+
+ if $field.data('height') != $field.outerHeight()
+ $field.data('height', $field.outerHeight())
+ autosize.destroy($field)
+ $field.css('max-height', window.outerHeight)
+
+ autosize($fields)
+ autosize.update($fields)
+
+ $fields.css('resize', 'vertical')
diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee
index 4ec8531d580..6e29d374267 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js.coffee
+++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee
@@ -1,29 +1,52 @@
# Quick Submit behavior
#
-# When an input field with the `js-quick-submit` class receives a "Meta+Enter"
-# (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, its parent form is
-# submitted.
+# When a child field of a form with a `js-quick-submit` class receives a
+# "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
+# is submitted.
#
#= require extensions/jquery
#
# ### Example Markup
#
-# <form action="/foo">
-# <input type="text" class="js-quick-submit" />
-# <textarea class="js-quick-submit"></textarea>
+# <form action="/foo" class="js-quick-submit">
+# <input type="text" />
+# <textarea></textarea>
+# <input type="submit" value="Submit" />
# </form>
#
+isMac = ->
+ navigator.userAgent.match(/Macintosh/)
+
+keyCodeIs = (e, keyCode) ->
+ return false if (e.originalEvent && e.originalEvent.repeat) || e.repeat
+ return e.keyCode == keyCode
+
$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
- return if (e.originalEvent && e.originalEvent.repeat) || e.repeat
- return unless e.keyCode == 13 # Enter
+ return unless keyCodeIs(e, 13) # Enter
- if navigator.userAgent.match(/Macintosh/)
- return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey)
- else
- return unless (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
+ return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
e.preventDefault()
$form = $(e.target).closest('form')
$form.find('input[type=submit], button[type=submit]').disable()
$form.submit()
+
+# If the user tabs to a submit button on a `js-quick-submit` form, display a
+# tooltip to let them know they could've used the hotkey
+$(document).on 'keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', (e) ->
+ return unless keyCodeIs(e, 9) # Tab
+
+ if isMac()
+ title = "You can also press &#8984;-Enter"
+ else
+ title = "You can also press Ctrl-Enter"
+
+ $this = $(@)
+ $this.tooltip(
+ container: 'body'
+ html: 'true'
+ placement: 'auto top'
+ title: title
+ trigger: 'manual'
+ ).tooltip('show').one('blur', -> $this.tooltip('hide'))
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index f6bf836f19f..390e41ed8d4 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -32,6 +32,7 @@ class @EditBlob
content: editor.getValue()
, (response) ->
currentPane.empty().append response
+ currentPane.syntaxHighlight()
return
else
diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee
index 917228bd276..f2fd2a775a4 100644
--- a/app/assets/javascripts/branch-graph.js.coffee
+++ b/app/assets/javascripts/branch-graph.js.coffee
@@ -66,7 +66,7 @@ class @BranchGraph
r.rect(40, 0, 30, @barHeight).attr fill: "#444"
for day, mm in @days
- if cuday isnt day[0]
+ if cuday isnt day[0] || cumonth isnt day[1]
# Dates
r.text(55, @offsetY + @unitTime * mm, day[0])
.attr(
diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee
new file mode 100644
index 00000000000..5457430f921
--- /dev/null
+++ b/app/assets/javascripts/breakpoints.coffee
@@ -0,0 +1,37 @@
+class @Breakpoints
+ instance = null;
+
+ class BreakpointInstance
+ BREAKPOINTS = ["xs", "sm", "md", "lg"]
+
+ constructor: ->
+ @setup()
+
+ setup: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ return if $(allDeviceSelector.join(",")).length
+
+ # Create all the elements
+ els = $.map BREAKPOINTS, (breakpoint) ->
+ "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
+ $("body").append els.join('')
+
+ visibleDevice: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ $(allDeviceSelector.join(",")).filter(":visible")
+
+ getBreakpointSize: ->
+ $visibleDevice = @visibleDevice
+ # the page refreshed via turbolinks
+ if not $visibleDevice().length
+ @setup()
+ $visibleDevice = @visibleDevice()
+ return $visibleDevice.attr("class").split("visible-")[1]
+
+ @get: ->
+ return instance ?= new BreakpointInstance
+
+$ =>
+ @bp = Breakpoints.get()
diff --git a/app/assets/javascripts/broadcast_message.js.coffee b/app/assets/javascripts/broadcast_message.js.coffee
new file mode 100644
index 00000000000..a38a329c4c2
--- /dev/null
+++ b/app/assets/javascripts/broadcast_message.js.coffee
@@ -0,0 +1,22 @@
+$ ->
+ $('input#broadcast_message_color').on 'input', ->
+ previewColor = $(@).val()
+ $('div.broadcast-message-preview').css('background-color', previewColor)
+
+ $('input#broadcast_message_font').on 'input', ->
+ previewColor = $(@).val()
+ $('div.broadcast-message-preview').css('color', previewColor)
+
+ previewPath = $('textarea#broadcast_message_message').data('preview-path')
+
+ $('textarea#broadcast_message_message').on 'input', ->
+ message = $(@).val()
+
+ if message == ''
+ $('.js-broadcast-message-preview').text("Your message here")
+ else
+ $.ajax(
+ url: previewPath
+ type: "POST"
+ data: { broadcast_message: { message: message } }
+ )
diff --git a/app/assets/javascripts/build_artifacts.js.coffee b/app/assets/javascripts/build_artifacts.js.coffee
new file mode 100644
index 00000000000..5ae6cba56c8
--- /dev/null
+++ b/app/assets/javascripts/build_artifacts.js.coffee
@@ -0,0 +1,14 @@
+class @BuildArtifacts
+ constructor: () ->
+ @disablePropagation()
+ @setupEntryClick()
+
+ disablePropagation: ->
+ $('.top-block').on 'click', '.download', (e) ->
+ e.stopPropagation()
+ $('.tree-holder').on 'click', 'tr[data-link] a', (e) ->
+ e.stopImmediatePropagation()
+
+ setupEntryClick: ->
+ $('.tree-holder').on 'click', 'tr[data-link]', (e) ->
+ window.location = @dataset.link
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
index 97621236924..d80e0e716ce 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/calendar.js.coffee
@@ -1,9 +1,4 @@
class @Calendar
- options =
- month: "short"
- day: "numeric"
- year: "numeric"
-
constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
cal = new CalHeatMap()
cal.init
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index 44d5ddb7d95..7afe8bf79e2 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -4,6 +4,8 @@ class CiBuild
constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval)
+ @initScrollButtonAffix()
+
if build_status == "running" || build_status == "pending"
#
# Bind autoscroll button to follow build output
@@ -38,4 +40,15 @@ class CiBuild
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
+ initScrollButtonAffix: ->
+ $buildScroll = $('#js-build-scroll')
+ $body = $('body')
+ $buildTrace = $('#build-trace')
+
+ $buildScroll.affix(
+ offset:
+ bottom: ->
+ $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
+ )
+
@CiBuild = CiBuild
diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee
index c183e78e513..ffd3627b1b0 100644
--- a/app/assets/javascripts/commits.js.coffee
+++ b/app/assets/javascripts/commits.js.coffee
@@ -1,15 +1,5 @@
class @CommitsList
- @data =
- ref: null
- limit: 0
- offset: 0
- @disable = false
-
- @showProgress: ->
- $('.loading').show()
-
- @hideProgress: ->
- $('.loading').hide()
+ @timer = null
@init: (ref, limit) ->
$("body").on "click", ".day-commits-table li.commit", (event) ->
@@ -18,38 +8,32 @@ class @CommitsList
e.stopPropagation()
return false
- @data.ref = ref
- @data.limit = limit
- @data.offset = limit
+ Pager.init limit, false
+
+ @content = $("#commits-list")
+ @searchField = $("#commits-search")
+ @initSearch()
- this.initLoadMore()
- this.showProgress()
+ @initSearch: ->
+ @timer = null
+ @searchField.keyup =>
+ clearTimeout(@timer)
+ @timer = setTimeout(@filterResults, 500)
+
+ @filterResults: =>
+ form = $(".commits-search-form")
+ search = @searchField.val()
+ commitsUrl = form.attr("action") + '?' + form.serialize()
+ @content.fadeTo('fast', 0.5)
- @getOld: ->
- this.showProgress()
$.ajax
type: "GET"
- url: location.href
- data: @data
- complete: this.hideProgress
- success: (data) ->
- CommitsList.append(data.count, data.html)
+ url: form.attr("action")
+ data: form.serialize()
+ complete: =>
+ @content.fadeTo('fast', 1.0)
+ success: (data) =>
+ @content.html(data.html)
+ # Change url so if user reload a page - search results are saved
+ history.replaceState {page: commitsUrl}, document.title, commitsUrl
dataType: "json"
-
- @append: (count, html) ->
- $("#commits-list").append(html)
- if count > 0
- @data.offset += count
- else
- @disable = true
-
- @initLoadMore: ->
- $(document).unbind('scroll')
- $(document).endlessScroll
- bottomPixels: 400
- fireDelay: 1000
- fireOnce: true
- ceaseFire: =>
- @disable
- callback: =>
- this.getOld()
diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee
deleted file mode 100644
index 00ee503ff16..00000000000
--- a/app/assets/javascripts/dashboard.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-class @Dashboard
- constructor: ->
- new ProjectsList()
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 69e061ce6e9..f5e1ca9860d 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()
@@ -23,8 +22,10 @@ class Dispatcher
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
- when 'projects:milestones:show'
+ 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'))
@@ -57,8 +58,6 @@ class Dispatcher
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
MergeRequests.init()
- when 'dashboard:show', 'root:show'
- new Dashboard()
when 'dashboard:activity'
new Activities()
when 'dashboard:projects:starred'
@@ -74,8 +73,11 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
- when 'groups:show'
+
+ new TreeView() if $('#tree-slider').length
+ when 'groups:activity'
new Activities()
+ when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index'
new GroupMembers()
@@ -86,9 +88,11 @@ class Dispatcher
when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
new GroupAvatar()
when 'projects:tree:show'
- new TreeView()
shortcut_handler = new ShortcutsNavigation()
- when 'projects:blob:show'
+ new TreeView()
+ when 'projects:find_file:show'
+ shortcut_handler = true
+ when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit'
@@ -99,9 +103,10 @@ class Dispatcher
shortcut_handler = true
when 'projects:forks:new'
new ProjectFork()
- when 'users:show'
- new User()
- new Activities()
+ when 'projects:artifacts:browse'
+ new BuildArtifacts()
+ when 'projects:group_links:index'
+ new GroupsSelect()
switch path.first()
when 'admin'
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 30a35a04339..b502131a99d 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -65,8 +65,7 @@ class @DropzoneInput
return
success: (header, response) ->
- child = $(dropzone[0]).children("textarea")
- $(child).val $(child).val() + formatLink(response.link) + "\n"
+ pasteText response.link.markdown
return
error: (temp, errorMessage) ->
@@ -99,11 +98,6 @@ class @DropzoneInput
child = $(dropzone[0]).children("textarea")
- formatLink = (link) ->
- text = "[#{link.alt}](#{link.url})"
- text = "!#{text}" if link.is_image
- text
-
handlePaste = (event) ->
pasteEvent = event.originalEvent
if pasteEvent.clipboardData and pasteEvent.clipboardData.items
@@ -133,6 +127,7 @@ class @DropzoneInput
beforeSelection = $(child).val().substring 0, caretStart
afterSelection = $(child).val().substring caretEnd, textEnd
$(child).val beforeSelection + text + afterSelection
+ child.get(0).setSelectionRange caretStart + text.length, caretEnd + text.length
form_textarea.trigger "input"
getFilename = (e) ->
@@ -162,7 +157,7 @@ class @DropzoneInput
closeAlertMessage()
success: (e, textStatus, response) ->
- insertToTextArea(filename, formatLink(response.responseJSON.link))
+ insertToTextArea(filename, response.responseJSON.link.markdown)
error: (response) ->
showError(response.responseJSON.message)
@@ -202,8 +197,3 @@ class @DropzoneInput
e.preventDefault()
$(@).closest('.gfm-form').find('.div-dropzone').click()
return
-
- formatLink: (link) ->
- text = "[#{link.alt}](#{link.url})"
- text = "!#{text}" if link.is_image
- text
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 7967892f856..4718bcf7a1e 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -34,7 +34,7 @@ GitLab.GfmAutoComplete =
searchKey: 'search'
callbacks:
beforeSave: (members) ->
- $.map members, (m) ->
+ $.map members, (m) ->
title = m.name
title += " (#{m.count})" if m.count
@@ -50,7 +50,7 @@ GitLab.GfmAutoComplete =
insertTpl: '${atwho-at}${id}'
callbacks:
beforeSave: (issues) ->
- $.map issues, (i) ->
+ $.map issues, (i) ->
id: i.iid
title: sanitize(i.title)
search: "#{i.iid} #{i.title}"
@@ -63,12 +63,12 @@ GitLab.GfmAutoComplete =
insertTpl: '${atwho-at}${id}'
callbacks:
beforeSave: (merges) ->
- $.map merges, (m) ->
+ $.map merges, (m) ->
id: m.iid
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
- input.one 'focus', =>
+ if @dataSource
$.getJSON(@dataSource).done (data) ->
# load members
input.atwho 'load', '@', data.members
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
new file mode 100644
index 00000000000..c81e8bf760a
--- /dev/null
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -0,0 +1,276 @@
+class GitLabDropdownFilter
+ BLUR_KEYCODES = [27, 40]
+
+ constructor: (@dropdown, @options) ->
+ @input = @dropdown.find(".dropdown-input .dropdown-input-field")
+
+ # Key events
+ timeout = ""
+ @input.on "keyup", (e) =>
+ if e.keyCode is 13 && @input.val() isnt ""
+ if @options.enterCallback
+ @options.enterCallback()
+ return
+
+ clearTimeout timeout
+ timeout = setTimeout =>
+ blur_field = @shouldBlur e.keyCode
+ search_text = @input.val()
+
+ if blur_field
+ @input.blur()
+
+ if @options.remote
+ @options.query search_text, (data) =>
+ @options.callback(data)
+ else
+ @filter search_text
+ , 250
+
+ shouldBlur: (keyCode) ->
+ return BLUR_KEYCODES.indexOf(keyCode) >= 0
+
+ filter: (search_text) ->
+ data = @options.data()
+ results = data
+
+ if search_text isnt ""
+ results = fuzzaldrinPlus.filter(data, search_text,
+ key: @options.keys
+ )
+
+ @options.callback results
+
+class GitLabDropdownRemote
+ constructor: (@dataEndpoint, @options) ->
+
+ execute: ->
+ if typeof @dataEndpoint is "string"
+ @fetchData()
+ else if typeof @dataEndpoint is "function"
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data by calling the data funcfion
+ @dataEndpoint "", (data) =>
+ if @options.success
+ @options.success(data)
+
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data through ajax if the data is a string
+ fetchData: ->
+ $.ajax(
+ url: @dataEndpoint,
+ dataType: @options.dataType,
+ beforeSend: =>
+ if @options.beforeSend
+ @options.beforeSend()
+ success: (data) =>
+ if @options.success
+ @options.success(data)
+ )
+
+class GitLabDropdown
+ LOADING_CLASS = "is-loading"
+ PAGE_TWO_CLASS = "is-page-two"
+ ACTIVE_CLASS = "is-active"
+
+ constructor: (@el, @options) ->
+ self = @
+ @dropdown = $(@el).parent()
+ 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
+
+ @parseData @fullData
+ }
+
+ # Init filiterable
+ if @options.filterable
+ @filter = new GitLabDropdownFilter @dropdown,
+ remote: @options.filterRemote
+ query: @options.data
+ keys: @options.search.fields
+ data: =>
+ return @fullData
+ callback: (data) =>
+ @parseData data
+ enterCallback: =>
+ @selectFirstRow()
+
+ # Event listeners
+ @dropdown.on "shown.bs.dropdown", @opened
+ @dropdown.on "hidden.bs.dropdown", @hidden
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
+ e.preventDefault()
+ e.stopPropagation()
+
+ @togglePage()
+
+ if @options.selectable
+ selector = ".dropdown-content a"
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content a"
+
+ @dropdown.on "click", selector, (e) ->
+ self.rowClicked $(@)
+
+ if self.options.clicked
+ self.options.clicked()
+
+ toggleLoading: ->
+ $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
+
+ togglePage: ->
+ menu = $('.dropdown-menu', @dropdown)
+
+ if menu.hasClass(PAGE_TWO_CLASS)
+ if @remote
+ @remote.execute()
+
+ menu.toggleClass PAGE_TWO_CLASS
+
+ parseData: (data) ->
+ @renderedData = data
+
+ # Render each row
+ html = $.map data, (obj) =>
+ return @renderItem(obj)
+
+ if @options.filterable and data.length is 0
+ # render no matching results
+ html = [@noResults()]
+
+ # Render the full menu
+ full_html = @renderMenu(html.join(""))
+
+ @appendMenu(full_html)
+
+ opened: =>
+ contentHtml = $('.dropdown-content', @dropdown).html()
+ if @remote && contentHtml is ""
+ @remote.execute()
+
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").focus()
+
+ hidden: =>
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").blur().val("")
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
+
+
+ # Render the full menu
+ renderMenu: (html) ->
+ menu_html = ""
+
+ if @options.renderMenu
+ menu_html = @options.renderMenu(html)
+ else
+ menu_html = "<ul>#{html}</ul>"
+
+ return menu_html
+
+ # Append the menu into the dropdown
+ appendMenu: (html) ->
+ selector = '.dropdown-content'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content"
+
+ $(selector, @dropdown).html html
+
+ # Render the row
+ renderItem: (data) ->
+ html = ""
+
+ return "<li class='divider'></li>" if data is "divider"
+
+ 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 ""
+ cssClass = "";
+
+ if selected
+ cssClass = "is-active"
+
+ html = "<li>"
+ html += "<a href='#{url}' class='#{cssClass}'>"
+ html += text
+ html += "</a>"
+ html += "</li>"
+
+ return html
+
+ noResults: ->
+ html = "<li>"
+ html += "<a href='#' class='is-focused'>"
+ html += "No matching results."
+ html += "</a>"
+ html += "</li>"
+
+ rowClicked: (el) ->
+ fieldName = @options.fieldName
+ field = @dropdown.parent().find("input[name='#{fieldName}']")
+
+ if el.hasClass(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
+
+ if !value?
+ field.remove()
+
+ if @options.multiSelect
+ oldValue = field.val()
+ if oldValue
+ value = "#{oldValue},#{value}"
+ else
+ @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
+
+ # Toggle active class for the tick mark
+ el.toggleClass "is-active"
+
+ # 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}' />"
+ @dropdown.before input
+
+ @dropdown.parent().find("input[name='#{fieldName}']").val value
+
+ 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
+ $(selector).trigger "click"
+
+$.fn.glDropdown = (opts) ->
+ return @.each ->
+ new GitLabDropdown @, opts
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index 02232698bc2..e52b73f94f6 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -10,20 +10,10 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit()
- $('.issuable-details').waitForImages ->
- $('.issuable-affix').on 'affix.bs.affix', ->
- $(@).width($(@).outerWidth())
- .on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
- $(@).width('')
-
- $('.issuable-affix').affix offset:
- top: ->
- @top = ($('.issuable-affix').offset().top - 70)
- bottom: ->
- @bottom = $('.footer').outerHeight(true)
-
- $(".edit-link").click (e) ->
+ $(document).on "click",".edit-link", (e) ->
block = $(@).parents('.block')
block.find('.selectbox').show()
block.find('.value').hide()
block.find('.js-select2').select2("open")
+
+ $(".right-sidebar").niceScroll()
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index c256ec8f41b..d663e34871c 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -6,22 +6,40 @@ class @Issue
constructor: ->
# Prevent duplicate event bindings
@disableTaskList()
-
+ @fixAffixScroll()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
+ fixAffixScroll: ->
+ fixAffix = ->
+ $discussion = $('.issuable-discussion')
+ $sidebar = $('.issuable-sidebar')
+ if $sidebar.hasClass('no-affix')
+ $sidebar.removeClass(['affix-top','affix'])
+ discussionHeight = $discussion.height()
+ sidebarHeight = $sidebar.height()
+ if sidebarHeight > discussionHeight
+ $discussion.height(sidebarHeight + 50)
+ $sidebar.addClass('no-affix')
+ $(window).on('resize', fixAffix)
+ fixAffix()
+
initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
initIssueBtnEventListeners: ->
+ _this = @
issueFailMessage = 'Unable to update this issue at this time.'
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
e.preventDefault()
e.stopImmediatePropagation()
$this = $(this)
isClose = $this.hasClass('btn-close')
+ shouldSubmit = $this.hasClass('btn-comment')
+ if shouldSubmit
+ _this.submitNoteForm($this.closest('form'))
$this.prop('disabled', true)
url = $this.attr('href')
$.ajax
@@ -32,12 +50,14 @@ class @Issue
new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) ->
if data.saved
- $this.addClass('hidden')
+ $(document).trigger('issuable:change');
if isClose
+ $('a.btn-close').addClass('hidden')
$('a.btn-reopen').removeClass('hidden')
$('div.status-box-closed').removeClass('hidden')
$('div.status-box-open').addClass('hidden')
else
+ $('a.btn-reopen').addClass('hidden')
$('a.btn-close').removeClass('hidden')
$('div.status-box-closed').addClass('hidden')
$('div.status-box-open').removeClass('hidden')
@@ -45,6 +65,11 @@ class @Issue
new Flash(issueFailMessage, 'alert')
$this.prop('disabled', false)
+ submitNoteForm: (form) =>
+ noteText = form.find("textarea.js-note-text").val()
+ if noteText.trim().length > 0
+ form.submit()
+
disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee
new file mode 100644
index 00000000000..c5740f27ddd
--- /dev/null
+++ b/app/assets/javascripts/issue_status_select.js.coffee
@@ -0,0 +1,11 @@
+class @IssueStatusSelect
+ constructor: ->
+ $('.js-issue-status').each (i, el) ->
+ fieldName = $(el).data("field-name")
+
+ $(el).glDropdown(
+ selectable: true
+ fieldName: fieldName
+ id: (obj, el) ->
+ $(el).data("id")
+ )
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index ac9e022e727..1127b289264 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -15,13 +15,6 @@
$(this).html totalIssues + 1
else
$(this).html totalIssues - 1
- $("body").on "click", ".issues-other-filters .dropdown-menu a", ->
- $('.issues-list').block(
- message: null,
- overlayCSS:
- backgroundColor: '#DDD'
- opacity: .4
- )
reload: ->
Issues.initSelects()
@@ -48,24 +41,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
new file mode 100644
index 00000000000..e6c1446f14f
--- /dev/null
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -0,0 +1,109 @@
+class @LabelsSelect
+ constructor: ->
+ $('.js-label-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ labelUrl = $dropdown.data('labels')
+ 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')
+ defaultLabel = $dropdown.text().trim()
+
+ if newLabelField.length
+ $('.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')
+ .addClass 'is-active'
+
+ $('.js-new-label-btn').on 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
+ $('.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()
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ $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 showNo
+ data.unshift(
+ id: 0
+ title: 'No Label'
+ )
+
+ 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 '' and label.title is selectedLbl
+ selected = 'is-active'
+ else
+ selected = if label.title is selectedLabel then 'is-active' else ''
+
+ "<li>
+ <a href='#' class='#{selected}'>
+ #{label.title}
+ </a>
+ </li>"
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ toggleLabel: (selected) ->
+ if selected and selected.title isnt 'Any Label'
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
+ id: (label) ->
+ if label.isAny?
+ ''
+ else
+ label.title
+ clicked: ->
+ 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)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
new file mode 100644
index 00000000000..d14b7139237
--- /dev/null
+++ b/app/assets/javascripts/logo.js.coffee
@@ -0,0 +1,50 @@
+Turbolinks.enableProgressBar();
+
+defaultClass = 'tanuki-shape'
+pieces = [
+ 'path#tanuki-right-cheek',
+ 'path#tanuki-right-eye, path#tanuki-right-ear',
+ 'path#tanuki-nose',
+ 'path#tanuki-left-eye, path#tanuki-left-ear',
+ 'path#tanuki-left-cheek',
+]
+pieceIndex = 0
+firstPiece = pieces[0]
+
+currentTimer = null
+delay = 150
+
+clearHighlights = ->
+ $(".#{defaultClass}.highlight").attr('class', defaultClass)
+
+start = ->
+ clearHighlights()
+ pieceIndex = 0
+ pieces.reverse() unless pieces[0] == firstPiece
+ clearInterval(currentTimer) if currentTimer
+ currentTimer = setInterval(work, delay)
+
+stop = ->
+ clearInterval(currentTimer)
+ clearHighlights()
+
+work = ->
+ clearHighlights()
+ $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight")
+
+ # If we hit the last piece, reset the index and then reverse the array to
+ # get a nice back-and-forth sweeping look
+ if pieceIndex == pieces.length - 1
+ pieceIndex = 0
+ pieces.reverse()
+ else
+ pieceIndex++
+
+$(document).on('page:fetch', start)
+$(document).on('page:change', stop)
+
+$ ->
+ # Make logo clickable as part of a workaround for Safari visited
+ # link behaviour (See !2690).
+ $('#logo').on 'click', ->
+ $('#js-shortcuts-home').get(0).click()
diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee
index 98fc8f17340..2a0b9479445 100644
--- a/app/assets/javascripts/markdown_preview.js.coffee
+++ b/app/assets/javascripts/markdown_preview.js.coffee
@@ -6,6 +6,7 @@
class @MarkdownPreview
# Minimum number of users referenced before triggering a warning
referenceThreshold: 10
+ ajaxCache: {}
showPreview: (form) ->
preview = form.find('.js-md-preview')
@@ -24,12 +25,16 @@ class @MarkdownPreview
renderMarkdown: (text, success) ->
return unless window.markdown_preview_path
+ return success(@ajaxCache.response) if text == @ajaxCache.text
+
$.ajax
type: 'POST'
url: window.markdown_preview_path
data: { text: text }
dataType: 'json'
- success: success
+ success: (response) =>
+ @ajaxCache = text: text, response: response
+ success(response)
hideReferencedUsers: (form) ->
referencedUsers = form.find('.referenced-users')
@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview()
previewButtonSelector = '.js-md-preview-button'
writeButtonSelector = '.js-md-write-button'
+lastTextareaPreviewed = null
$.fn.setupMarkdownPreview = ->
$form = $(this)
@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = ->
form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
form_textarea.on 'blur', -> markdownPreview.showPreview($form)
-$(document).on 'click', previewButtonSelector, (e) ->
- e.preventDefault()
+$(document).on 'markdown-preview:show', (e, $form) ->
+ return unless $form
- $form = $(this).closest('form')
+ lastTextareaPreviewed = $form.find('textarea.markdown-area')
# toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active')
@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) ->
markdownPreview.showPreview($form)
-$(document).on 'click', writeButtonSelector, (e) ->
- e.preventDefault()
+$(document).on 'markdown-preview:hide', (e, $form) ->
+ return unless $form
- $form = $(this).closest('form')
+ lastTextareaPreviewed = null
# toggle tabs
$form.find(writeButtonSelector).parent().addClass('active')
@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) ->
# toggle content
$form.find('.md-write-holder').show()
+ $form.find('textarea.markdown-area').focus()
$form.find('.md-preview-holder').hide()
+
+$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
+ $target = $(keyboardEvent.target)
+
+ if $target.is('textarea.markdown-area')
+ $(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
+ keyboardEvent.preventDefault()
+ else if lastTextareaPreviewed
+ $target = lastTextareaPreviewed
+ $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
+ keyboardEvent.preventDefault()
+
+$(document).on 'click', previewButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ $(document).triggerHandler('markdown-preview:show', [$form])
+
+$(document).on 'click', writeButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ $(document).triggerHandler('markdown-preview:hide', [$form])
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 9047587db81..6af5a48a0bb 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -15,10 +15,13 @@ class @MergeRequest
this.$('.show-all-commits').on 'click', =>
this.showAllCommits()
+ @fixAffixScroll();
+
@initTabs()
# Prevent duplicate event bindings
@disableTaskList()
+ @initMRBtnListeners()
if $("a.btn-close").length
@initTaskList()
@@ -27,6 +30,20 @@ class @MergeRequest
$: (selector) ->
this.$el.find(selector)
+ fixAffixScroll: ->
+ fixAffix = ->
+ $discussion = $('.issuable-discussion')
+ $sidebar = $('.issuable-sidebar')
+ if $sidebar.hasClass('no-affix')
+ $sidebar.removeClass(['affix-top','affix'])
+ discussionHeight = $discussion.height()
+ sidebarHeight = $sidebar.height()
+ if sidebarHeight > discussionHeight
+ $discussion.height(sidebarHeight + 50)
+ $sidebar.addClass('no-affix')
+ $(window).on('resize', fixAffix)
+ fixAffix()
+
initTabs: ->
if @opts.action != 'new'
# `MergeRequests#new` has no tab-persisting or lazy-loading behavior
@@ -43,6 +60,28 @@ class @MergeRequest
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
+ initMRBtnListeners: ->
+ _this = @
+ $('a.btn-close, a.btn-reopen').on 'click', (e) ->
+ $this = $(this)
+ shouldSubmit = $this.hasClass('btn-comment')
+ if shouldSubmit && $this.data('submitted')
+ return
+ if shouldSubmit
+ if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ _this.submitNoteForm($this.closest('form'),$this)
+
+
+ submitNoteForm: (form, $button) =>
+ noteText = form.find("textarea.js-note-text").val()
+ if noteText.trim().length > 0
+ form.submit()
+ $button.data('submitted',true)
+ $button.trigger('click')
+
+
disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 9e2dc1250c9..8322b4c46ad 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -5,7 +5,7 @@
#
# ### Example Markup
#
-# <ul class="nav nav-tabs merge-request-tabs">
+# <ul class="nav-links merge-request-tabs">
# <li class="notes-tab active">
# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
# Discussion
@@ -70,6 +70,7 @@ class @MergeRequestTabs
@loadCommits($target.attr('href'))
else if action == 'diffs'
@loadDiff($target.attr('href'))
+ @shrinkView()
else if action == 'builds'
@loadBuilds($target.attr('href'))
@@ -145,7 +146,9 @@ class @MergeRequestTabs
url: "#{source}.json" + @_location.search
success: (data) =>
document.querySelector("div#diffs").innerHTML = data.html
+ $('.js-timeago').timeago()
$('div#diffs .js-syntax-highlight').syntaxHighlight()
+ @expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true
@scrollToElement("#diffs")
@@ -177,3 +180,20 @@ class @MergeRequestTabs
options = $.extend({}, defaults, options)
$.ajax(options)
+
+ # Returns diff view type
+ diffViewType: ->
+ $('.inline-parallel-buttons a.active').data('view-type')
+
+ expandViewContainer: ->
+ $('.container-fluid').removeClass('container-limited')
+
+ shrinkView: ->
+ $gutterIcon = $('.js-sidebar-toggle i')
+
+ # Wait until listeners are set
+ setTimeout( ->
+ # Only when sidebar is collapsed
+ if $gutterIcon.is('.fa-angle-double-right')
+ $gutterIcon.closest('a').trigger('click',[true])
+ , 0)
diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee
index 83434c1b9ba..b3c73ffce5d 100644
--- a/app/assets/javascripts/merge_requests.js.coffee
+++ b/app/assets/javascripts/merge_requests.js.coffee
@@ -16,7 +16,7 @@
form = $("#issue_search_form")
search = $("#issue_search").val()
$('.merge-requests-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '? '+ form.serialize()
+ issues_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
@@ -27,7 +27,7 @@
success: (data) ->
$('.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: issues_url}, document.title, issues_url
MergeRequests.reload()
dataType: "json"
diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee
index d644d50b669..0037a3a21c2 100644
--- a/app/assets/javascripts/milestone.js.coffee
+++ b/app/assets/javascripts/milestone.js.coffee
@@ -62,14 +62,24 @@ class @Milestone
dataType: "json"
constructor: ->
+ oldMouseStart = $.ui.sortable.prototype._mouseStart
+ $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) ->
+ this._trigger "beforeStart", event, this._uiHash()
+ oldMouseStart.apply this, [event, overrideHandle, noActivation]
+
@bindIssuesSorting()
@bindMergeRequestSorting()
+ @bindTabsSwitching()
bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
connectWith: ".issues-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
+ beforeStart: (event, ui) ->
+ $(".issues-sortable-list").css "min-height", ui.item.outerHeight()
+ stop: (event, ui) ->
+ $(".issues-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortIssues(data)
@@ -94,11 +104,24 @@ class @Milestone
).disableSelection()
+ bindTabsSwitching: ->
+ $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
+ currentTabClass = $(e.target).data('show')
+ previousTabClass = $(e.relatedTarget).data('show')
+
+ $(previousTabClass).hide()
+ $(currentTabClass).removeClass('hidden')
+ $(currentTabClass).show()
+
bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
+ beforeStart: (event, ui) ->
+ $(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight()
+ stop: (event, ui) ->
+ $(".merge_requests-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortMergeRequests(data)
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
new file mode 100644
index 00000000000..0287d98b1ec
--- /dev/null
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -0,0 +1,74 @@
+class @MilestoneSelect
+ constructor: ->
+ $('.js-milestone-select').each (i, dropdown) ->
+ $dropdown = $(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')
+ defaultLabel = $dropdown.text().trim()
+
+ $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()
+ )
+
+ if showNo
+ data.unshift(
+ id: '0'
+ title: 'No Milestone'
+ )
+
+ if showAny
+ data.unshift(
+ isAny: true
+ title: 'Any Milestone'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, 'divider'
+
+ callback(data)
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ 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.isAny?
+ milestone.title
+ else
+ ''
+ else
+ milestone.id
+ isSelected: (milestone) ->
+ milestone.title is selectedMilestone
+ clicked: ->
+ 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)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 9e5204bfeeb..b164231e7ef 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -1,4 +1,5 @@
#= require autosave
+#= require autosize
#= require dropzone
#= require dropzone_input
#= require gfm_auto_complete
@@ -14,10 +15,14 @@ class @Notes
@last_fetched_at = last_fetched_at
@view = view
@noteable_url = document.URL
- @initRefresh()
- @setupMainTargetNoteForm()
+ @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge")
+ @basePollingInterval = 15000
+ @maxPollingSteps = 4
+
@cleanBinding()
@addBinding()
+ @setPollingInterval()
+ @setupMainTargetNoteForm()
@initTaskList()
addBinding: ->
@@ -25,18 +30,19 @@ class @Notes
$(document).on "ajax:success", ".js-main-target-form", @addNote
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
+ # catch note ajax errors
+ $(document).on "ajax:error", ".js-main-target-form", @addNoteError
+
# change note in UI after update
- $(document).on "ajax:success", "form.edit_note", @updateNote
+ $(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link
$(document).on "click", ".js-note-edit", @showEditForm
$(document).on "click", ".note-edit-cancel", @cancelEdit
# Reopen and close actions for Issue/MR combined with note form submit
- $(document).on "click", ".js-note-target-reopen", @targetReopen
- $(document).on "click", ".js-note-target-close", @targetClose
$(document).on "click", ".js-comment-button", @updateCloseButton
- $(document).on "keyup", ".js-note-text", @updateTargetButtons
+ $(document).on "keyup input", ".js-note-text", @updateTargetButtons
# remove a note (in general)
$(document).on "click", ".js-note-delete", @removeNote
@@ -48,6 +54,9 @@ class @Notes
$(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
$(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
+ # reset main target form when clicking discard
+ $(document).on "click", ".js-note-discard", @resetMainTargetForm
+
# update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment
@@ -63,10 +72,13 @@ class @Notes
# fetch notes when tab becomes visible
$(document).on "visibilitychange", @visibilityChange
+ # when issue status changes, we need to refresh data
+ $(document).on "issuable:change", @refresh
+
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
- $(document).off "ajax:success", "form.edit_note"
+ $(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete"
@@ -79,6 +91,7 @@ class @Notes
$(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
+ $(document).off "click", ".js-note-discard"
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
@@ -87,10 +100,12 @@ class @Notes
clearInterval(Notes.interval)
Notes.interval = setInterval =>
@refresh()
- , 15000
+ , @pollingInterval
refresh: ->
- unless document.hidden or (@noteable_url != document.URL)
+ return if @refreshing is true
+ refreshing = true
+ if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent()
getContent: ->
@@ -101,9 +116,31 @@ class @Notes
success: (data) =>
notes = data.notes
@last_fetched_at = data.last_fetched_at
+ @setPollingInterval(data.notes.length)
$.each notes, (i, note) =>
- @renderNote(note)
+ if note.discussion_with_diff_html?
+ @renderDiscussionNote(note)
+ else
+ @renderNote(note)
+ always: =>
+ @refreshing = false
+ ###
+ Increase @pollingInterval up to 120 seconds on every function call,
+ if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
+ will reset to @basePollingInterval.
+
+ Note: this function is used to gradually increase the polling interval
+ if there aren't new notes coming from the server
+ ###
+ setPollingInterval: (shouldReset = true) ->
+ nthInterval = @basePollingInterval * Math.pow(2, @maxPollingSteps - 1)
+ if shouldReset
+ @pollingInterval = @basePollingInterval
+ else if @pollingInterval < nthInterval
+ @pollingInterval *= 2
+
+ @initRefresh()
###
Render note in main comments area.
@@ -117,18 +154,21 @@ class @Notes
flash.pinTo('.header-content')
return
+ if note.award
+ awards_handler.addAwardToEmojiBar(note.note)
+ awards_handler.scrollToAwards()
+
# render note if it not present in loaded list
# or skip if rendered
- if @isNewNote(note) && !note.award
+ else if @isNewNote(note)
@note_ids.push(note.id)
- $('ul.main-notes-list').
- append(note.html).
- syntaxHighlight()
+
+ $('ul.main-notes-list')
+ .append(note.html)
+ .syntaxHighlight()
@initTaskList()
+ @updateNotesCount(1)
- if note.award
- awards_handler.addAwardToEmojiBar(note.note)
- awards_handler.scrollToAwards()
###
Check if note does not exists on page
@@ -145,34 +185,39 @@ class @Notes
Note: for rendering inline notes use renderDiscussionNote
###
renderDiscussionNote: (note) ->
+ return unless @isNewNote(note)
+
@note_ids.push(note.id)
- form = $("form[rel='" + note.discussion_id + "']")
+ form = $("#new-discussion-note-form-#{note.discussion_id}")
row = form.closest("tr")
note_html = $(note.html)
note_html.syntaxHighlight()
# is this the first note of discussion?
- if row.is(".js-temp-notes-holder")
+ discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
+ if discussionContainer.length is 0
# insert the note and the reply button after the temp row
row.after note.discussion_html
# remove the note (will be added again below)
row.next().find(".note").remove()
+ # Before that, the container didn't exist
+ discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
+
# Add note to 'Changes' page discussions
- $(".notes[rel='" + note.discussion_id + "']").append note_html
+ discussionContainer.append note_html
# Init discussion on 'Discussion' page if it is merge request page
- if $('body').attr('data-page').indexOf('projects:merge_request') == 0
- discussion_html = $(note.discussion_with_diff_html)
- discussion_html.syntaxHighlight()
- $('ul.main-notes-list').append(discussion_html)
+ if $('body').attr('data-page').indexOf('projects:merge_request') is 0
+ $('ul.main-notes-list')
+ .append(note.discussion_with_diff_html)
+ .syntaxHighlight()
else
# append new note to all matching discussions
- $(".notes[rel='" + note.discussion_id + "']").append note_html
+ discussionContainer.append note_html
- # cleanup after successfully creating a diff/discussion note
- @removeDiscussionNoteForm(form)
+ @updateNotesCount(1)
###
Called in response the main target form has been successfully submitted.
@@ -181,7 +226,7 @@ class @Notes
Resets text and preview.
Resets buttons.
###
- resetMainTargetForm: ->
+ resetMainTargetForm: (e) =>
form = $(".js-main-target-form")
# remove validation errors
@@ -193,6 +238,8 @@ class @Notes
form.find(".js-note-text").data("autosave").reset()
+ @updateTargetButtons(e)
+
reenableTargetFormSubmitButton: ->
form = $(".js-main-target-form")
@@ -236,8 +283,10 @@ class @Notes
form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove()
+ # hide discard button
+ form.find('.js-note-discard').hide()
+
# setup preview buttons
- form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text")
@@ -248,6 +297,7 @@ class @Notes
else
previewButton.removeClass("turn-on").addClass "turn-off"
+ autosize(textarea)
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
@@ -270,6 +320,10 @@ class @Notes
addNote: (xhr, note, status) =>
@renderNote(note)
+ addNoteError: (xhr, note, status) =>
+ flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
+ flash.pinTo('.md-area')
+
###
Called in response to the new note form being submitted
@@ -278,6 +332,9 @@ class @Notes
addDiscussionNote: (xhr, note, status) =>
@renderDiscussionNote(note)
+ # cleanup after successfully creating a diff/discussion note
+ @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}"))
+
###
Called in response to the edit note form being submitted
@@ -286,6 +343,7 @@ class @Notes
updateNote: (_xhr, note, _status) =>
# Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html)
+ $('.js-timeago', $html).timeago()
$html.syntaxHighlight()
$html.find('.js-task-list-container').taskList('enable')
@@ -305,22 +363,27 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").hide()
note.find(".note-header").hide()
- base_form = note.find(".note-edit-form")
- form = base_form.clone().insertAfter(base_form)
- form.addClass('current-note-edit-form gfm-form')
- form.find('.div-dropzone').remove()
+ 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()
# Setup markdown form
- GitLab.GfmAutoComplete.setup()
- new DropzoneInput(form)
+ if isNewForm
+ GitLab.GfmAutoComplete.setup()
+ new DropzoneInput(form)
- form.show()
textarea = form.find("textarea")
textarea.focus()
+ if isNewForm
+ autosize(textarea)
+
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we
# modify it, so let's clear it and re-set it!
@@ -328,7 +391,8 @@ class @Notes
textarea.val ""
textarea.val value
- disableButtonIfEmptyField textarea, form.find(".js-comment-button")
+ if isNewForm
+ disableButtonIfEmptyField textarea, form.find(".js-comment-button")
###
Called in response to clicking the edit note link
@@ -340,7 +404,9 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").show()
note.find(".note-header").show()
- note.find(".current-note-edit-form").remove()
+ note.find(".current-note-edit-form")
+ .removeClass("current-note-edit-form")
+ .hide()
###
Called in response to deleting a note of any kind.
@@ -348,29 +414,32 @@ class @Notes
Removes the actual note from view.
Removes the whole discussion if the last note is being removed.
###
- removeNote: ->
- note = $(this).closest(".note")
- note_id = note.attr('id')
+ removeNote: (e) =>
+ noteId = $(e.currentTarget)
+ .closest(".note")
+ .attr("id")
- $('.note[id="' + note_id + '"]').each ->
- note = $(this)
+ # A same note appears in the "Discussion" and in the "Changes" tab, we have
+ # to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
+ # where $("#noteId") would return only one.
+ $(".note[id='#{noteId}']").each (i, el) =>
+ note = $(el)
notes = note.closest(".notes")
- count = notes.closest(".notes_holder").find(".discussion-notes-count")
# check if this is the last note for this line
if notes.find(".note").length is 1
- # for discussions
- notes.closest(".discussion").remove()
+ # "Discussions" tab
+ notes.closest(".timeline-entry").remove()
- # for diff lines
+ # "Changes" tab / commit view
notes.closest("tr").remove()
- else
- # update notes count
- count.get(0).lastChild.nodeValue = " #{notes.children().length - 1}"
note.remove()
+ # Decrement the "Discussions" counter only once
+ @updateNotesCount(-1)
+
###
Called in response to clicking the delete attachment link
@@ -410,12 +479,17 @@ class @Notes
###
setupDiscussionNoteForm: (dataHolder, form) =>
# setup note target
- form.attr "rel", dataHolder.data("discussionId")
+ form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
form.find("#line_type").val dataHolder.data("lineType")
form.find("#note_commit_id").val dataHolder.data("commitId")
form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId")
+ form.find('.js-note-discard')
+ .show()
+ .removeClass('js-note-discard')
+ .addClass('js-close-discussion-note-form')
+ .text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form
form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form"
@@ -512,32 +586,55 @@ class @Notes
visibilityChange: =>
@refresh()
- targetReopen: (e) =>
- @submitNoteForm($(e.target).parents('form'))
-
- targetClose: (e) =>
- @submitNoteForm($(e.target).parents('form'))
-
- submitNoteForm: (form) =>
- noteText = form.find(".js-note-text").val()
- if noteText.trim().length > 0
- form.submit()
-
updateCloseButton: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
- form.find('.js-note-target-close').text('Close')
+ closebtn = form.find('.js-note-target-close')
+ closebtn.text(closebtn.data('original-text'))
updateTargetButtons: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
+ reopenbtn = form.find('.js-note-target-reopen')
+ closebtn = form.find('.js-note-target-close')
+ discardbtn = form.find('.js-note-discard')
if textarea.val().trim().length > 0
- form.find('.js-note-target-reopen').text('Comment & reopen')
- form.find('.js-note-target-close').text('Comment & close')
+ reopentext = reopenbtn.data('alternative-text')
+ closetext = closebtn.data('alternative-text')
+
+ if reopenbtn.text() isnt reopentext
+ reopenbtn.text(reopentext)
+
+ if closebtn.text() isnt closetext
+ closebtn.text(closetext)
+
+ if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ reopenbtn.addClass('btn-comment-and-reopen')
+
+ if closebtn.is(':not(.btn-comment-and-close)')
+ closebtn.addClass('btn-comment-and-close')
+
+ if discardbtn.is(':hidden')
+ discardbtn.show()
else
- form.find('.js-note-target-reopen').text('Reopen')
- form.find('.js-note-target-close').text('Close')
+ reopentext = reopenbtn.data('original-text')
+ closetext = closebtn.data('original-text')
+
+ if reopenbtn.text() isnt reopentext
+ reopenbtn.text(reopentext)
+
+ if closebtn.text() isnt closetext
+ closebtn.text(closetext)
+
+ if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ reopenbtn.removeClass('btn-comment-and-reopen')
+
+ if closebtn.is(':not(.btn-comment-and-close)')
+ closebtn.removeClass('btn-comment-and-close')
+
+ if discardbtn.is(':visible')
+ discardbtn.hide()
initTaskList: ->
@enableTaskList()
@@ -548,3 +645,6 @@ class @Notes
updateTaskList: ->
$('form', this).submit()
+
+ updateNotesCount: (updateCount) ->
+ @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount)
diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee
index d639303aed3..0ff83b7f0c8 100644
--- a/app/assets/javascripts/pager.js.coffee
+++ b/app/assets/javascripts/pager.js.coffee
@@ -1,6 +1,7 @@
@Pager =
init: (@limit = 0, preload, @disable = false) ->
- @loading = $(".loading")
+ @loading = $('.loading').first()
+
if preload
@offset = 0
@getOld()
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index bb0b66b86e1..20f87440551 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -4,12 +4,13 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
- $('.update-username form').on 'ajax:before', ->
- $('.loading-gif').show()
+ $('.update-username').on 'ajax:before', ->
+ $('.loading-username').show()
$(this).find('.update-success').hide()
$(this).find('.update-failed').hide()
- $('.update-username form').on 'ajax:complete', ->
+ $('.update-username').on 'ajax:complete', ->
+ $('.loading-username').hide()
$(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide()
@@ -24,3 +25,12 @@ class @Profile
form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '')
form.find(".js-avatar-filename").text(filename)
+
+$ ->
+ # Extract the SSH Key title from its comment
+ $(document).on 'focusout.ssh_key', '#key_key', ->
+ $title = $('#key_title')
+ comment = $(@).val().match(/^\S+ \S+ (.+)\n?$/)
+
+ if comment && comment.length > 1 && $title.val() == ''
+ $title.val(comment[1]).change()
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index d7a658f8faa..76bc4ff42a2 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -50,3 +50,19 @@ class @Project
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
$(@).parents('ul').find('li.active').removeClass 'active'
$(@).parent().addClass 'active'
+
+ @projectSelectDropdown()
+
+ projectSelectDropdown: ->
+ new ProjectSelect()
+
+ $('.project-item-select').on 'click', (e) =>
+ @changeProject $(e.currentTarget).val()
+
+ $('.js-projects-dropdown-toggle').on 'click', (e) ->
+ e.preventDefault()
+
+ $('.js-projects-dropdown').select2('open')
+
+ changeProject: (url) ->
+ window.location = url
diff --git a/app/assets/javascripts/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee
new file mode 100644
index 00000000000..0dd32352c34
--- /dev/null
+++ b/app/assets/javascripts/project_find_file.js.coffee
@@ -0,0 +1,125 @@
+class @ProjectFindFile
+ constructor: (@element, @options)->
+ @filePaths = {}
+ @inputElement = @element.find(".file-finder-input")
+
+ # init event
+ @initEvent()
+
+ # focus text input box
+ @inputElement.focus()
+
+ # load file list
+ @load(@options.url)
+
+ # init event
+ initEvent: ->
+ @inputElement.off "keyup"
+ @inputElement.on "keyup", (event) =>
+ target = $(event.target)
+ value = target.val()
+ oldValue = target.data("oldValue") ? ""
+
+ if value != oldValue
+ target.data("oldValue", value)
+ @findFile()
+ @element.find("tr.tree-item").eq(0).addClass("selected").focus()
+
+ @element.find(".tree-content-holder .tree-table").on "click", (event) ->
+ if (event.target.nodeName != "A")
+ path = @element.find(".tree-item-file-name a", this).attr("href")
+ location.href = path if path
+
+ # find file
+ findFile: ->
+ searchText = @inputElement.val()
+ result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths
+ @renderList result, searchText
+
+ # files pathes load
+ load: (url) ->
+ $.ajax
+ url: url
+ method: "get"
+ dataType: "json"
+ success: (data) =>
+ @element.find(".loading").hide()
+ @filePaths = data
+ @findFile()
+ @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus()
+
+ # render result
+ renderList: (filePaths, searchText) ->
+ @element.find(".tree-table > tbody").empty()
+
+ for filePath, i in filePaths
+ break if i == 20
+
+ if searchText
+ matches = fuzzaldrinPlus.match(filePath, searchText)
+
+ blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}"
+
+ html = @makeHtml filePath, matches, blobItemUrl
+ @element.find(".tree-table > tbody").append(html)
+
+ # highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
+ highlighter = (element, text, matches) ->
+ lastIndex = 0
+ highlightText = ""
+ matchedChars = []
+
+ for matchIndex in matches
+ unmatched = text.substring(lastIndex, matchIndex)
+
+ if unmatched
+ element.append(matchedChars.join("").bold()) if matchedChars.length
+ matchedChars = []
+ element.append(document.createTextNode(unmatched))
+
+ matchedChars.push(text[matchIndex])
+ lastIndex = matchIndex + 1
+
+ element.append(matchedChars.join("").bold()) if matchedChars.length
+ element.append(document.createTextNode(text.substring(lastIndex)))
+
+ # make tbody row html
+ makeHtml: (filePath, matches, blobItemUrl) ->
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>")
+ if matches
+ $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl))
+ else
+ $tr.find("a").attr("href", blobItemUrl).text(filePath)
+
+ return $tr
+
+ selectRow: (type) ->
+ rows = @element.find(".files-slider tr.tree-item")
+ selectedRow = @element.find(".files-slider tr.tree-item.selected")
+
+ if rows && rows.length > 0
+ if selectedRow && selectedRow.length > 0
+ if type == "UP"
+ next = selectedRow.prev()
+ else if type == "DOWN"
+ next = selectedRow.next()
+
+ if next.length > 0
+ selectedRow.removeClass "selected"
+ selectedRow = next
+ else
+ selectedRow = rows.eq(0)
+ selectedRow.addClass("selected").focus()
+
+ selectRowUp: =>
+ @selectRow "UP"
+
+ selectRowDown: =>
+ @selectRow "DOWN"
+
+ goToTree: =>
+ location.href = @options.treeUrl
+
+ goToBlob: =>
+ path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href")
+ location.href = path if path
diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee
index fecdb9fc2e7..63dee4ed5d7 100644
--- a/app/assets/javascripts/project_new.js.coffee
+++ b/app/assets/javascripts/project_new.js.coffee
@@ -3,3 +3,16 @@ class @ProjectNew
$('.project-edit-container').on 'ajax:before', =>
$('.project-edit-container').hide()
$('.save-project-loader').show()
+ @toggleSettings()
+ @toggleSettingsOnclick()
+
+
+ toggleSettings: ->
+ checked = $("#project_builds_enabled").prop("checked")
+ if checked
+ $('.builds-feature').show()
+ else
+ $('.builds-feature').hide()
+
+ toggleSettingsOnclick: ->
+ $("#project_builds_enabled").on 'click', @toggleSettings
diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee
index 0ae274f3363..be8ab9b428d 100644
--- a/app/assets/javascripts/project_select.js.coffee
+++ b/app/assets/javascripts/project_select.js.coffee
@@ -3,6 +3,7 @@ class @ProjectSelect
$('.ajax-project-select').each (i, select) ->
@groupId = $(select).data('group-id')
@includeGroups = $(select).data('include-groups')
+ @orderBy = $(select).data('order-by') || 'id'
placeholder = "Search for project"
placeholder += " or group" if @includeGroups
@@ -28,7 +29,7 @@ class @ProjectSelect
if @groupId
Api.groupProjects @groupId, query.term, projectsCallback
else
- Api.projects query.term, projectsCallback
+ Api.projects query.term, @orderBy, projectsCallback
id: (project) ->
project.web_url
diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee
index f2887af190b..e4c4bf3b273 100644
--- a/app/assets/javascripts/projects_list.js.coffee
+++ b/app/assets/javascripts/projects_list.js.coffee
@@ -1,24 +1,37 @@
-class @ProjectsList
- constructor: ->
- $(".projects-list .js-expand").on 'click', (e) ->
- e.preventDefault()
- list = $(this).closest('.projects-list')
- list.find("li").show()
- list.find("li.bottom").hide()
+@ProjectsList =
+ init: ->
+ $(".projects-list-filter").off('keyup')
+ this.initSearch()
+ this.initPagination()
- $(".projects-list-filter").keyup ->
- terms = $(this).val()
- uiBox = $('div.projects-list-holder')
- if terms == "" || terms == undefined
- uiBox.find("ul.projects-list li").show()
- else
- uiBox.find("ul.projects-list li").each (index) ->
- name = $(this).find("span.filter-title").text()
+ initSearch: ->
+ @timer = null
+ $(".projects-list-filter").on('keyup', ->
+ clearTimeout(@timer)
+ @timer = setTimeout(ProjectsList.filterResults, 500)
+ )
- if name.toLowerCase().search(terms.toLowerCase()) == -1
- $(this).hide()
- else
- $(this).show()
- uiBox.find("ul.projects-list li.bottom").hide()
+ filterResults: =>
+ $('.projects-list-holder').fadeTo(250, 0.5)
+ form = null
+ form = $("form#project-filter-form")
+ search = $(".projects-list-filter").val()
+ project_filter_url = form.attr('action') + '?' + form.serialize()
+ $.ajax
+ type: "GET"
+ url: form.attr('action')
+ data: form.serialize()
+ complete: ->
+ $('.projects-list-holder').fadeTo(250, 1)
+ success: (data) ->
+ $('.projects-list-holder').replaceWith(data.html)
+ # Change url so if user reload a page - search results are saved
+ history.replaceState {page: project_filter_url}, document.title, project_filter_url
+ dataType: "json"
+
+ initPagination: ->
+ $('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
+ $('.projects-list-holder').replaceWith(data.html)
+ )
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index 4d915bfc8c5..100e3aac535 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -4,16 +4,23 @@ class @Shortcuts
Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
+ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
+ Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
+ toggleMarkdownPreview: (e) =>
+ $(document).triggerHandler('markdown-preview:toggle', [e])
+
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
else
+ url = '/help/shortcuts'
+ url = gon.relative_url_root + url if gon.relative_url_root?
$.ajax(
- url: '/help/shortcuts',
+ url: url,
dataType: 'script',
success: (e) ->
if location and location.length > 0
@@ -32,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
+
+Mousetrap.stopCallback = (->
+ defaultStopCallback = Mousetrap.stopCallback
+
+ return (e, element, combo) ->
+ # allowed shortcuts if textarea, input, contenteditable are focused
+ if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
+ return false
+ else
+ return defaultStopCallback.apply(@, arguments)
+)()
diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee
new file mode 100644
index 00000000000..311e80bae19
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_find_file.js.coffee
@@ -0,0 +1,19 @@
+#= require shortcuts_navigation
+
+class @ShortcutsFindFile extends ShortcutsNavigation
+ constructor: (@projectFindFile) ->
+ super()
+ _oldStopCallback = Mousetrap.stopCallback
+ # override to fire shortcuts action when focus in textbox
+ Mousetrap.stopCallback = (event, element, combo) =>
+ if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter')
+ # when press up/down key in textbox, cusor prevent to move to home/end
+ event.preventDefault()
+ return false
+
+ return _oldStopCallback(event, element, combo)
+
+ Mousetrap.bind('up', @projectFindFile.selectRowUp)
+ Mousetrap.bind('down', @projectFindFile.selectRowDown)
+ Mousetrap.bind('esc', @projectFindFile.goToTree)
+ Mousetrap.bind('enter', @projectFindFile.goToBlob)
diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee
index bb532194682..bbf02f1db24 100644
--- a/app/assets/javascripts/shortcuts_issuable.coffee
+++ b/app/assets/javascripts/shortcuts_issuable.coffee
@@ -5,23 +5,46 @@ class @ShortcutsIssuable extends ShortcutsNavigation
constructor: (isMergeRequest) ->
super()
Mousetrap.bind('a', ->
- $('.js-assignee').select2('open')
+ $('.block.assignee .edit-link').trigger('click')
return false
)
Mousetrap.bind('m', ->
- $('.js-milestone').select2('open')
+ $('.block.milestone .edit-link').trigger('click')
return false
)
Mousetrap.bind('r', =>
@replyWithSelectedText()
return false
)
+ Mousetrap.bind('j', =>
+ @prevIssue()
+ return false
+ )
+ Mousetrap.bind('k', =>
+ @nextIssue()
+ return false
+ )
+ Mousetrap.bind('e', =>
+ @editIssue()
+ return false
+ )
+
if isMergeRequest
@enabledHelp.push('.hidden-shortcut.merge_requests')
else
@enabledHelp.push('.hidden-shortcut.issues')
+ prevIssue: ->
+ $prevBtn = $('.prev-btn')
+ if not $prevBtn.hasClass('disabled')
+ Turbolinks.visit($prevBtn.attr('href'))
+
+ nextIssue: ->
+ $nextBtn = $('.next-btn')
+ if not $nextBtn.hasClass('disabled')
+ Turbolinks.visit($nextBtn.attr('href'))
+
replyWithSelectedText: ->
if window.getSelection
selected = window.getSelection().toString()
@@ -44,3 +67,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation
# Focus the input field
replyField.focus()
+
+ editIssue: ->
+ $editBtn = $('.issuable-edit')
+ Turbolinks.visit($editBtn.attr('href'))
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index ae59480af9e..eea3f5ee910 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -1,11 +1,27 @@
-$(document).on("click", '.toggle-nav-collapse', (e) ->
- e.preventDefault()
- collapsed = 'page-sidebar-collapsed'
- expanded = 'page-sidebar-expanded'
+collapsed = 'page-sidebar-collapsed'
+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 ( ->
+ niceScrollBars = $('.nicescroll').niceScroll();
+ niceScrollBars.updateScrollBar();
+ ), 300
+
+$(document).on("click", '.toggle-nav-collapse', (e) ->
+ e.preventDefault()
+
+ toggleSidebar()
)
+
+$ ->
+ size = bp.getBreakpointSize()
+
+ if size is "xs" or size is "sm"
+ if $('.page-with-sidebar').hasClass(expanded)
+ toggleSidebar()
diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee
index d849b2e7950..f27780dda93 100644
--- a/app/assets/javascripts/star.js.coffee
+++ b/app/assets/javascripts/star.js.coffee
@@ -6,7 +6,7 @@ class @Star
$starIcon = $this.find('i')
toggleStar = (isStarred) ->
- $this.parent().find('span.count').text data.star_count
+ $this.parent().find('.star-count').text data.star_count
if isStarred
$starSpan.removeClass('starred').text 'Star'
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
@@ -19,4 +19,4 @@ class @Star
return
).on 'ajax:error', (e, xhr, status, error) ->
new Flash('Star toggle failed. Try again later.', 'alert')
- return \ No newline at end of file
+ return
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
index 7f41616d4e7..084f0e0dc65 100644
--- a/app/assets/javascripts/subscription.js.coffee
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -1,17 +1,21 @@
class @Subscription
- constructor: (url) ->
- $(".subscribe-button").unbind("click").click (event)=>
- btn = $(event.currentTarget)
- action = btn.find("span").text()
- current_status = $(".subscription-status").attr("data-status")
- btn.prop("disabled", true)
-
- $.post url, =>
- btn.prop("disabled", false)
- status = if current_status == "subscribed" then "unsubscribed" else "subscribed"
- $(".subscription-status").attr("data-status", status)
- action = if status == "subscribed" then "Unsubscribe" else "Subscribe"
- btn.find("span").text(action)
- $(".subscription-status>div").toggleClass("hidden")
+ constructor: (container) ->
+ $container = $(container)
+ @url = $container.attr('data-url')
+ @subscribe_button = $container.find('.subscribe-button')
+ @subscription_status = $container.find('.subscription-status')
+ @subscribe_button.unbind('click').click(@toggleSubscription)
-
+ toggleSubscription: (event) =>
+ btn = $(event.currentTarget)
+ action = btn.find('span').text()
+ current_status = @subscription_status.attr('data-status')
+ btn.prop('disabled', true)
+
+ $.post @url, =>
+ btn.prop('disabled', false)
+ status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
+ @subscription_status.attr('data-status', status)
+ action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
+ btn.find('span').text(action)
+ @subscription_status.find('>div').toggleClass('hidden')
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
new file mode 100644
index 00000000000..b6b4bd90e6a
--- /dev/null
+++ b/app/assets/javascripts/todos.js.coffee
@@ -0,0 +1,56 @@
+class @Todos
+ constructor: (@name) ->
+ @clearListeners()
+ @initBtnListeners()
+
+ clearListeners: ->
+ $('.done-todo').off('click')
+ $('.js-todos-mark-all').off('click')
+
+ initBtnListeners: ->
+ $('.done-todo').on('click', @doneClicked)
+ $('.js-todos-mark-all').on('click', @allDoneClicked)
+
+ 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
diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee
index ec4271b092c..2882a90d118 100644
--- a/app/assets/javascripts/user.js.coffee
+++ b/app/assets/javascripts/user.js.coffee
@@ -1,10 +1,17 @@
class @User
- constructor: ->
+ constructor: (@opts) ->
$('.profile-groups-avatars').tooltip("placement": "top")
- new ProjectsList()
+
+ @initTabs()
$('.hide-project-limit-message').on 'click', (e) ->
path = '/'
$.cookie('hide_project_limit_message', 'false', { path: path })
$(@).parents('.project-limit-message').remove()
e.preventDefault()
+
+ initTabs: ->
+ new UserTabs(
+ parentEl: '.user-profile'
+ action: @opts.action
+ )
diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee
new file mode 100644
index 00000000000..09b7eec9104
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js.coffee
@@ -0,0 +1,146 @@
+# UserTabs
+#
+# Handles persisting and restoring the current tab selection and lazily-loading
+# content on the Users#show page.
+#
+# ### Example Markup
+#
+# <ul class="nav-links">
+# <li class="activity-tab active">
+# <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+# Activity
+# </a>
+# </li>
+# <li class="groups-tab">
+# <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+# Groups
+# </a>
+# </li>
+# <li class="contributed-tab">
+# <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
+# Contributed projects
+# </a>
+# </li>
+# <li class="projects-tab">
+# <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
+# Personal projects
+# </a>
+# </li>
+# </ul>
+#
+# <div class="tab-content">
+# <div class="tab-pane" id="activity">
+# Activity Content
+# </div>
+# <div class="tab-pane" id="groups">
+# Groups Content
+# </div>
+# <div class="tab-pane" id="contributed">
+# Contributed projects content
+# </div>
+# <div class="tab-pane" id="projects">
+# Projects content
+# </div>
+# </div>
+#
+# <div class="loading-status">
+# <div class="loading">
+# Loading Animation
+# </div>
+# </div>
+#
+class @UserTabs
+ constructor: (opts) ->
+ {
+ @action = 'activity'
+ @defaultAction = 'activity'
+ @parentEl = $(document)
+ } = opts
+
+ # Make jQuery object if selector is provided
+ @parentEl = $(@parentEl) if typeof @parentEl is 'string'
+
+ # Store the `location` object, allowing for easier stubbing in tests
+ @_location = location
+
+ # Set tab states
+ @loaded = {}
+ for item in @parentEl.find('.nav-links a')
+ @loaded[$(item).attr 'data-action'] = false
+
+ # Actions
+ @actions = Object.keys @loaded
+
+ @bindEvents()
+
+ # Set active tab
+ @action = @defaultAction if @action is 'show'
+ @activateTab(@action)
+
+ bindEvents: ->
+ # Toggle event listeners
+ @parentEl
+ .off 'shown.bs.tab', '.nav-links a[data-toggle="tab"]'
+ .on 'shown.bs.tab', '.nav-links a[data-toggle="tab"]', @tabShown
+
+ tabShown: (event) =>
+ $target = $(event.target)
+ action = $target.data('action')
+ source = $target.attr('href')
+
+ @setTab(source, action)
+ @setCurrentAction(action)
+
+ activateTab: (action) ->
+ @parentEl.find(".nav-links .#{action}-tab a").tab('show')
+
+ setTab: (source, action) ->
+ return if @loaded[action] is true
+
+ if action is 'activity'
+ @loadActivities(source)
+
+ if action in ['groups', 'contributed', 'projects']
+ @loadTab(source, action)
+
+ loadTab: (source, action) ->
+ $.ajax
+ beforeSend: => @toggleLoading(true)
+ complete: => @toggleLoading(false)
+ dataType: 'json'
+ type: 'GET'
+ url: "#{source}.json"
+ success: (data) =>
+ tabSelector = 'div#' + action
+ @parentEl.find(tabSelector).html(data.html)
+ @loaded[action] = true
+
+ loadActivities: (source) ->
+ return if @loaded['activity'] is true
+
+ $calendarWrap = @parentEl.find('.user-calendar')
+ $calendarWrap.load($calendarWrap.data('href'))
+
+ new Activities()
+ @loaded['activity'] = true
+
+ toggleLoading: (status) ->
+ @parentEl.find('.loading-status .loading').toggle(status)
+
+ setCurrentAction: (action) ->
+ # Remove possible actions from URL
+ regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$')
+ new_state = @_location.pathname
+ new_state = new_state.replace(/\/+$/, "") # remove trailing slashes
+ new_state = new_state.replace(regExp, '')
+
+ # Append the new action if we're on a tab other than 'activity'
+ unless action == @defaultAction
+ new_state += "/#{action}"
+
+ # Ensure parameters and hash come along for the ride
+ new_state += @_location.search + @_location.hash
+
+ history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
+
+ new_state
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 9467011799f..48831dd6bc4 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -3,6 +3,94 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
+ $('.js-user-search').each (i, dropdown) =>
+ $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.text().trim()
+
+ $dropdown.glDropdown(
+ data: (term, callback) =>
+ @users term, (users) =>
+ if term.length is 0
+ showDivider = 0
+
+ if firstUser
+ # Move current user to the front of the list
+ for obj, index in users
+ if obj.username == firstUser
+ users.splice(index, 1)
+ users.unshift(obj)
+ break
+
+ if showNullUser
+ showDivider += 1
+ users.unshift(
+ name: 'Unassigned',
+ id: 0
+ )
+
+ if showAnyUser
+ showDivider += 1
+ name = showAnyUser
+ name = 'Any User' if name == true
+ anyUser = {
+ name: name,
+ id: null
+ }
+ users.unshift(anyUser)
+
+ if showDivider
+ users.splice(showDivider, 0, "divider")
+
+ # Send the data back
+ callback users
+ filterable: true
+ filterRemote: true
+ search:
+ fields: ['name', 'username']
+ selectable: true
+ fieldName: $dropdown.data('field-name')
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.name
+ else
+ defaultLabel
+ clicked: ->
+ 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)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ 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' />"
+
+ "<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'>
+ #{username}
+ </span>
+ </a>
+ </li>"
+ )
+
$('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id')
@groupId = $(select).data('group-id')
diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee
index 81cfc37b956..1ee827f1fa3 100644
--- a/app/assets/javascripts/wikis.js.coffee
+++ b/app/assets/javascripts/wikis.js.coffee
@@ -1,17 +1,19 @@
+#= require latinise
+
class @Wikis
constructor: ->
- $('.build-new-wiki').bind "click", (e) ->
- $('[data-error~=slug]').addClass("hidden")
- $('p.hint').show()
+ $('.new-wiki-page').on 'submit', (e) =>
+ $('[data-error~=slug]').addClass('hidden')
field = $('#new_wiki_path')
- valid_slug_pattern = /^[\w\/-]+$/
+ slug = @slugify(field.val())
- slug = field.val()
- if slug.match valid_slug_pattern
+ if (slug.length > 0)
path = field.attr('data-wikis-path')
- if(slug.length > 0)
- location.href = path + "/" + slug
- else
+ location.href = path + '/' + slug
e.preventDefault()
- $('p.hint').hide()
- $('[data-error~=slug]').removeClass("hidden")
+
+ dasherize: (value) ->
+ value.replace(/[_\s]+/g, '-')
+
+ slugify: (value) =>
+ @dasherize(value.trim().toLowerCase().latinise())
diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee
index a1462cf3cae..e1c5446eaac 100644
--- a/app/assets/javascripts/zen_mode.js.coffee
+++ b/app/assets/javascripts/zen_mode.js.coffee
@@ -1,56 +1,80 @@
+# Zen Mode (full screen) textarea
+#
+#= provides zen_mode:enter
+#= provides zen_mode:leave
+#
+#= require jquery.scrollTo
#= require dropzone
#= require mousetrap
#= require mousetrap/pause
-
+#
+# ### Events
+#
+# `zen_mode:enter`
+#
+# Fired when the "Edit in fullscreen" link is clicked.
+#
+# **Synchronicity** Sync
+# **Bubbles** Yes
+# **Cancelable** No
+# **Target** a.js-zen-enter
+#
+# `zen_mode:leave`
+#
+# Fired when the "Leave Fullscreen" link is clicked.
+#
+# **Synchronicity** Sync
+# **Bubbles** Yes
+# **Cancelable** No
+# **Target** a.js-zen-leave
+#
class @ZenMode
constructor: ->
- @active_zen_area = null
- @active_checkbox = null
- @scroll_position = 0
-
- $(window).scroll =>
- if not @active_checkbox
- @scroll_position = window.pageYOffset
+ @active_backdrop = null
+ @active_textarea = null
- $('body').on 'click', '.zen-enter-link', (e) =>
+ $(document).on 'click', '.js-zen-enter', (e) ->
e.preventDefault()
- $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true).change()
+ $(e.currentTarget).trigger('zen_mode:enter')
- $('body').on 'click', '.zen-leave-link', (e) =>
+ $(document).on 'click', '.js-zen-leave', (e) ->
e.preventDefault()
- $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false).change()
-
- $('body').on 'change', '.zen-toggle-comment', (e) =>
- checkbox = e.currentTarget
- if checkbox.checked
- # Disable other keyboard shortcuts in ZEN mode
- Mousetrap.pause()
- @updateActiveZenArea(checkbox)
- else
- @exitZenMode()
-
- $(document).on 'keydown', (e) =>
- if e.keyCode is 27 # Esc
- @exitZenMode()
+ $(e.currentTarget).trigger('zen_mode:leave')
+
+ $(document).on 'zen_mode:enter', (e) =>
+ @enter(e.target.parentNode)
+ $(document).on 'zen_mode:leave', (e) =>
+ @exit()
+
+ $(document).on 'keydown', (e) ->
+ if e.keyCode == 27 # Esc
e.preventDefault()
+ $(document).trigger('zen_mode:leave')
+
+ enter: (backdrop) ->
+ Mousetrap.pause()
+
+ @active_backdrop = $(backdrop)
+ @active_backdrop.addClass('fullscreen')
+
+ @active_textarea = @active_backdrop.find('textarea')
- updateActiveZenArea: (checkbox) =>
- @active_checkbox = $(checkbox)
- @active_checkbox.prop('checked', true)
- @active_zen_area = @active_checkbox.parent().find('textarea')
# Prevent a user-resized textarea from persisting to fullscreen
- @active_zen_area.removeAttr('style')
- @active_zen_area.focus()
+ @active_textarea.removeAttr('style')
+ @active_textarea.focus()
- exitZenMode: =>
- if @active_zen_area isnt null
+ exit: ->
+ if @active_textarea
Mousetrap.unpause()
- @active_checkbox.prop('checked', false)
- @active_zen_area = null
- @active_checkbox = null
- @restoreScroll(@scroll_position)
- # Enable dropzone when leaving ZEN mode
+
+ @active_textarea.closest('.zen-backdrop').removeClass('fullscreen')
+
+ @scrollTo(@active_textarea)
+
+ @active_textarea = null
+ @active_backdrop = null
+
Dropzone.forElement('.div-dropzone').enable()
- restoreScroll: (y) ->
- window.scrollTo(window.pageXOffset, y)
+ scrollTo: (zen_area) ->
+ $.scrollTo(zen_area, 0, offset: -150)
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 0c0451fe4dd..2d301d21ab9 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -25,12 +25,6 @@
@import "framework";
/*
- * NProgress load bar css
- */
-@import 'nprogress';
-@import 'nprogress-bootstrap';
-
-/*
* Font icons
*/
@import "font-awesome";
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 48a4971c8fc..c85ab9148d0 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -11,6 +11,7 @@
@import "framework/calendar.scss";
@import "framework/callout.scss";
@import "framework/common.scss";
+@import "framework/dropdowns.scss";
@import "framework/files.scss";
@import "framework/filters.scss";
@import "framework/flash.scss";
@@ -24,7 +25,9 @@
@import "framework/lists.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
+@import "framework/nav.scss";
@import "framework/pagination.scss";
+@import "framework/progress.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
@import "framework/sidebar.scss";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 36e582d4854..b7ffa3e6ffb 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -24,6 +24,7 @@
&.s26 { width: 26px; height: 26px; margin-right: 8px; }
&.s32 { width: 32px; height: 32px; margin-right: 10px; }
&.s36 { width: 36px; height: 36px; margin-right: 10px; }
+ &.s40 { width: 40px; height: 40px; margin-right: 10px; }
&.s46 { width: 46px; height: 46px; margin-right: 15px; }
&.s48 { width: 48px; height: 48px; margin-right: 10px; }
&.s60 { width: 60px; height: 60px; margin-right: 12px; }
@@ -40,7 +41,8 @@
&.s16 { font-size: 12px; line-height: 1.33; }
&.s24 { font-size: 14px; line-height: 1.8; }
&.s26 { font-size: 20px; line-height: 1.33; }
- &.s32 { font-size: 22px; line-height: 32px; }
+ &.s32 { font-size: 20px; line-height: 32px; }
+ &.s40 { font-size: 16px; line-height: 40px; }
&.s60 { font-size: 32px; line-height: 60px; }
&.s90 { font-size: 36px; line-height: 90px; }
&.s110 { font-size: 40px; line-height: 112px; font-weight: 300; }
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 206d39cc9b3..c36f29dda0e 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -18,12 +18,12 @@
line-height: 36px;
}
-.content-block,
.gray-content-block {
- margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: -$gl-padding;
background-color: $background-color;
padding: $gl-padding;
- margin-bottom: 0px;
+ margin-bottom: 0;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
color: $gl-gray;
@@ -66,21 +66,27 @@
}
.oneline {
- line-height: 42px;
+ line-height: 35px;
}
> p:last-child {
margin-bottom: 0;
}
+
+ .block-controls {
+ float: right;
+
+ .control {
+ float: left;
+ margin-left: 10px;
+ }
+ }
}
.cover-block {
text-align: center;
background: $background-color;
- margin: -$gl-padding;
- margin-bottom: 0;
- padding: 44px $gl-padding;
- border-bottom: 1px solid $border-color;
+ padding-top: 44px;
position: relative;
.avatar-holder {
@@ -110,6 +116,10 @@
.cover-desc {
padding: 0 $gl-padding 3px;
color: $gl-text-color;
+
+ &.username:last-child {
+ padding-bottom: $gl-padding;
+ }
}
.cover-controls {
@@ -127,3 +137,27 @@
.block-connector {
margin-top: -1px;
}
+
+.nav-block {
+ .controls {
+ float: right;
+ margin-top: 11px;
+ }
+}
+
+.content-block {
+ padding: $gl-padding 0;
+ border-bottom: 1px solid $border-color;
+
+ &.oneline-block {
+ line-height: 36px;
+ }
+
+ > .controls {
+ float: right;
+ }
+}
+
+.content-block-small {
+ padding: 10px 0;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 97a94638847..657c5f033c7 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,24 +1,18 @@
@mixin btn-default {
@include border-radius(3px);
- border-width: 1px;
- border-style: solid;
- font-size: 15px;
+ font-size: $gl-font-size;
font-weight: 500;
- line-height: 18px;
- padding: 11px $gl-padding;
- letter-spacing: .4px;
+ padding: $gl-vert-padding $gl-btn-padding;
&:focus,
&:active {
outline: none;
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow($gl-btn-active-background);
}
}
@mixin btn-middle {
@include btn-default;
- @include border-radius(3px);
- padding: 11px 24px;
}
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
@@ -34,7 +28,7 @@
}
&:active {
- @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow ($gl-btn-active-background);
background-color: $dark;
border-color: $border-dark;
@@ -43,23 +37,23 @@
}
@mixin btn-green {
- @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #FFFFFF);
+ @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff);
}
@mixin btn-blue {
- @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF);
+ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff);
}
@mixin btn-blue-medium {
- @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF);
+ @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff);
}
@mixin btn-orange {
- @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF);
+ @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff);
}
@mixin btn-red {
- @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #FFFFFF);
+ @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff);
}
@mixin btn-gray {
@@ -74,23 +68,27 @@
@include btn-default;
@include btn-white;
- &.btn-sm {
- padding: 5px 10px;
+ color: $gl-text-color;
+
+ &:focus:active {
+ outline: 0;
}
- &.btn-nr {
- padding: 7px 10px;
+ &.btn-small,
+ &.btn-sm {
+ padding: 4px 10px;
+ font-size: 13px;
+ line-height: 18px;
}
&.btn-xs {
- padding: 1px 5px;
+ padding: 2px 5px;
}
&.btn-success,
&.btn-new,
&.btn-create,
- &.btn-save,
- &.btn-green {
+ &.btn-save {
@include btn-green;
}
@@ -129,8 +127,31 @@
margin-right: 7px;
float: left;
&:last-child {
- margin-right: 0px;
+ margin-right: 0;
}
+ &.btn-xs {
+ margin-right: 3px;
+ }
+ }
+ &.disabled {
+ pointer-events: auto !important;
+ }
+
+ .caret {
+ margin-left: 5px;
+ }
+}
+
+.btn-transparent {
+ color: $btn-transparent-color;
+ background-color: transparent;
+ border: 0;
+
+ &:hover,
+ &:active,
+ &:focus {
+ background-color: transparent;
+ box-shadow: none;
}
}
@@ -148,38 +169,52 @@
margin-right: 7px;
float: left;
&:last-child {
- margin-right: 0px;
+ margin-right: 0;
}
}
}
-.btn-group-next {
+.btn-clipboard {
+ border: none;
+ padding: 0 5px;
+}
+
+.input-group-btn {
.btn {
- padding: 9px 0px;
- font-size: 15px;
- color: #7f8fa4;
- border-color: #e7e9ed;
- width: 140px;
-
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: #78a;
+ @include btn-middle;
+
+ &:hover {
+ outline: none;
}
- &.active {
- border-color: $gl-info;
- background: $gl-info;
- color: #fff;
+ &:focus {
+ outline: none;
+ }
+
+ &:active {
+ outline: none;
+ }
- .badge {
- color: $gl-info;
- background-color: white;
- }
+ &.btn-clipboard {
+ padding-left: 15px;
+ padding-right: 15px;
}
}
+
+ .active {
+ @include box-shadow($gl-btn-active-background);
+
+ border: 1px solid #c6cacf !important;
+ background-color: #e4e7ed !important;
+ }
}
-.btn-clipboard {
- border: none;
+.btn-loading {
+ &:not(.disabled) .fa {
+ display: none;
+ }
+
+ .fa {
+ margin-right: 5px;
+ }
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index a36fefe22c5..e3192823a1a 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -19,38 +19,33 @@
}
}
}
+
/**
* This overwrites the default values of the cal-heatmap gem
*/
.calendar {
.qi {
- background-color: #999;
fill: #fff;
}
.q1 {
- background-color: #dae289;
- fill: #ededed;
+ fill: #ededed !important;
}
.q2 {
- background-color: #cedb9c;
- fill: #ACD5F2;
+ fill: #acd5f2 !important;
}
.q3 {
- background-color: #b5cf6b;
- fill: #7FA8D1;
+ fill: #7fa8d1 !important;
}
.q4 {
- background-color: #637939;
- fill: #49729B;
+ fill: #49729b !important;
}
.q5 {
- background-color: #3b6427;
- fill: #254E77;
+ fill: #254e77 !important;
}
.domain-background {
@@ -59,32 +54,7 @@
}
.ch-tooltip {
- position: absolute;
- display: none;
- margin-top: 22px;
- margin-left: 1px;
- font-size: 13px;
padding: 3px;
font-weight: 550;
- background-color: #222;
- span {
- position: absolute;
- width: 200px;
- text-align: center;
- visibility: hidden;
- border-radius: 10px;
- &:after {
- content: '';
- position: absolute;
- top: 100%;
- left: 50%;
- margin-left: -8px;
- width: 0;
- height: 0;
- border-top: 8px solid #000000;
- border-right: 8px solid transparent;
- border-left: 8px solid transparent;
- }
- }
}
}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index 20a9bfb9816..da7bab74a32 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -39,6 +39,6 @@
}
.bs-callout-success {
background-color: #dff0d8;
- border-color: #5cA64d;
+ border-color: #5ca64d;
color: #3c763d;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 11730000f85..bc03c2180be 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,21 +1,27 @@
/** COLORS **/
.cgray { color: $gl-gray; }
-.clgray { color: #BBB }
+.clgray { color: #bbb }
.cred { color: $gl-text-red; }
.cgreen { color: $gl-text-green; }
.cdark { color: #444 }
/** COMMON CLASSES **/
-.prepend-top-10 { margin-top:10px }
+.prepend-top-0 { margin-top: 0; }
+.prepend-top-5 { margin-top: 5px; }
+.prepend-top-10 { margin-top: 10px }
.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top:20px }
-.prepend-left-10 { margin-left:10px }
-.prepend-left-20 { margin-left:20px }
-.append-right-10 { margin-right:10px }
-.append-right-20 { margin-right:20px }
-.append-bottom-10 { margin-bottom:10px }
-.append-bottom-15 { margin-bottom:15px }
-.append-bottom-20 { margin-bottom:20px }
+.prepend-top-20 { margin-top: 20px }
+.prepend-left-10 { margin-left: 10px }
+.prepend-left-default { margin-left: $gl-padding; }
+.prepend-left-20 { margin-left: 20px }
+.append-right-5 { margin-right: 5px }
+.append-right-10 { margin-right: 10px }
+.append-right-default { margin-right: $gl-padding; }
+.append-right-20 { margin-right: 20px }
+.append-bottom-0 { margin-bottom: 0 }
+.append-bottom-10 { margin-bottom: 10px }
+.append-bottom-15 { margin-bottom: 15px }
+.append-bottom-20 { margin-bottom: 20px }
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block }
.center { text-align: center }
@@ -45,7 +51,7 @@ pre {
}
&.well-pre {
- border: 1px solid #EEE;
+ border: 1px solid #eee;
background: #f9f9f9;
border-radius: 0;
color: #555;
@@ -56,25 +62,12 @@ hr {
margin: $gl-padding 0;
}
-.dropdown-menu > li > a {
- text-shadow: none;
-}
-
-.dropdown-menu-align-right {
- left: auto;
- right: 0px;
-}
-
-.dropdown-menu > li > a:hover,
-.dropdown-menu > li > a:focus {
- background: $gl-primary;
- color: #FFF;
-}
-
.str-truncated {
@include str-truncated;
}
+.item-title { font-weight: 600; }
+
/** FLASH message **/
.author_link {
color: $gl-link-color;
@@ -110,7 +103,7 @@ span.update-author {
}
.user-mention {
- color: #2FA0BB;
+ color: #2fa0bb;
font-weight: bold;
}
@@ -118,14 +111,6 @@ span.update-author {
display: inline;
}
-.line_holder {
- &:hover {
- td {
- background: #FFFFCF !important;
- }
- }
-}
-
p.time {
color: #999;
font-size: 90%;
@@ -149,10 +134,10 @@ p.time {
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
- img { max-width:100% }
+ img { max-width: 100% }
.note-title {
li {
- border-bottom:none !important;
+ border-bottom: none !important;
}
}
}
@@ -202,9 +187,9 @@ li.note {
.error-message {
padding: 10px;
- background: #C67;
+ background: #c67;
margin: 0;
- color: #FFF;
+ color: #fff;
a {
color: #fff;
@@ -215,7 +200,7 @@ li.note {
.browser-alert {
padding: 10px;
text-align: center;
- background: #C67;
+ background: #c67;
color: #fff;
font-weight: bold;
a {
@@ -286,7 +271,7 @@ img.emoji {
table {
td.permission-x {
- background: #D9EDF7 !important;
+ background: #d9edf7 !important;
text-align: center;
}
}
@@ -295,7 +280,7 @@ table {
float: left;
text-align: center;
font-size: 32px;
- color: #AAA;
+ color: #aaa;
width: 60px;
}
@@ -307,7 +292,7 @@ table {
}
.btn-sign-in {
- margin-top: 8px;
+ margin-top: 10px;
text-shadow: none;
}
@@ -317,14 +302,6 @@ table {
}
}
-.wiki .highlight, .note-body .highlight {
- margin: 12px 0 12px 0;
-}
-
-.wiki .code {
- overflow-x: auto;
-}
-
.footer-links {
margin-bottom: 20px;
a {
@@ -370,76 +347,7 @@ table {
.profiler-button,
.profiler-controls {
- border-color: #EEE !important;
- }
-}
-
-.center-top-menu, .left-top-menu {
- @include nav-menu;
- text-align: center;
- margin-top: 5px;
- margin-bottom: $gl-padding;
- height: auto;
- margin-top: -$gl-padding;
-
- &.no-bottom {
- margin-bottom: 0;
- }
-
- &.no-top {
- margin-top: 0;
- }
-
- li a {
- display: inline-block;
- padding-top: $gl-padding;
- padding-bottom: 11px;
- margin-bottom: -1px;
- }
-
- &.bottom-border {
- border-bottom: 1px solid $border-color;
- height: 57px;
- }
-
- &.wide {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- }
-}
-
-.left-top-menu {
- text-align: left;
- border-bottom: 1px solid #EEE;
-}
-
-.center-middle-menu {
- @include nav-menu;
- padding: 0;
- text-align: center;
- margin: -$gl-padding;
- margin-top: 0;
- margin-bottom: 0;
- height: 58px;
- border-bottom: 1px solid $border-color;
-
- li {
- &:after {
- content: "|";
- color: $border-gray-light;
- }
-
- &:last-child {
- &:after {
- content: none;
- }
- }
-
- > a {
- display: inline-block;
- text-transform: uppercase;
- font-size: 13px;
- }
+ border-color: #eee !important;
}
}
@@ -459,11 +367,11 @@ table {
margin-bottom: $gl-padding;
}
-.new-project-item-select-holder {
+.project-item-select-holder {
display: inline-block;
position: relative;
- .new-project-item-select {
+ .project-item-select {
position: absolute;
top: 0;
right: 0;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
new file mode 100644
index 00000000000..a48b6c17fa0
--- /dev/null
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -0,0 +1,348 @@
+.caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: $caret-width-base dashed;
+ border-right: $caret-width-base solid transparent;
+ border-left: $caret-width-base solid transparent;
+}
+
+.btn-group {
+ .caret {
+ margin-left: 0;
+ }
+}
+
+.dropdown {
+ position: relative;
+}
+
+.open {
+ .dropdown-menu {
+ display: block;
+ }
+
+ .dropdown-menu-toggle {
+ border-color: $dropdown-toggle-hover-border-color;
+
+ .fa {
+ color: $dropdown-toggle-hover-icon-color;
+ }
+ }
+}
+
+.dropdown-menu-toggle {
+ position: relative;
+ width: 160px;
+ padding: 6px 20px 6px 10px;
+ background-color: $dropdown-toggle-bg;
+ color: $dropdown-toggle-color;
+ font-size: 15px;
+ text-align: left;
+ border: 1px solid $dropdown-toggle-border-color;
+ border-radius: 2px;
+ outline: 0;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+
+ .fa {
+ position: absolute;
+ top: 50%;
+ right: 6px;
+ margin-top: -4px;
+ color: $dropdown-toggle-icon-color;
+ font-size: 10px;
+ }
+
+ &:hover, {
+ border-color: $dropdown-toggle-hover-border-color;
+
+ .fa {
+ color: $dropdown-toggle-hover-icon-color;
+ }
+ }
+}
+
+.dropdown-menu {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 9;
+ width: 240px;
+ margin-top: 2px;
+ margin-bottom: 0;
+ padding: 10px 10px;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: $dropdown-bg;
+ border: 1px solid $dropdown-border-color;
+ border-radius: $border-radius-base;
+ box-shadow: 0 2px 4px $dropdown-shadow-color;
+
+ &.is-loading {
+ .dropdown-content {
+ display: none;
+ }
+
+ .dropdown-loading {
+ display: block;
+ }
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ }
+
+ li {
+ text-align: left;
+ list-style: none;
+ }
+
+ .divider {
+ width: 100%;
+ height: 1px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ background-color: $dropdown-divider-color;
+ }
+
+ a {
+ display: block;
+ position: relative;
+ padding-left: 10px;
+ padding-right: 10px;
+ color: $dropdown-link-color;
+ line-height: 34px;
+ text-overflow: ellipsis;
+ border-radius: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+
+ &:hover,
+ &:focus,
+ &.is-focused {
+ background-color: $dropdown-link-hover-bg;
+ text-decoration: none;
+ outline: 0;
+ }
+ }
+}
+
+.dropdown-menu-paging {
+ .dropdown-page-two,
+ .dropdown-menu-back {
+ display: none;
+ }
+
+ &.is-page-two {
+ .dropdown-page-one {
+ display: none;
+ }
+
+ .dropdown-page-two,
+ .dropdown-menu-back {
+ display: block;
+ }
+ }
+}
+
+.dropdown-menu-user {
+ .avatar {
+ float: left;
+ width: 30px;
+ height: 30px;
+ margin: 0 10px 0 0;
+ }
+}
+
+.dropdown-menu-user-link {
+ padding-top: 7px;
+ padding-bottom: 7px;
+}
+
+.dropdown-menu-user-full-name {
+ display: block;
+ font-weight: 600;
+ line-height: 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.dropdown-menu-user-username {
+ display: block;
+ line-height: 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.dropdown-select {
+ width: 280px;
+}
+
+.dropdown-menu-align-right {
+ left: auto;
+ right: 0;
+}
+
+.dropdown-menu-selectable {
+ a {
+ padding-left: 25px;
+
+ &.is-active {
+ &::before {
+ content: "\f00c";
+ position: absolute;
+ left: 5px;
+ top: 50%;
+ margin-top: -7px;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ }
+ }
+}
+
+.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;
+ font-weight: 600;
+ line-height: 1;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-bottom: 1px solid $dropdown-divider-color;
+ overflow: hidden;
+}
+
+.dropdown-title-button {
+ position: absolute;
+ top: -1px;
+ padding: 0;
+ color: $dropdown-title-btn-color;
+ font-size: 14px;
+ border: 0;
+ background: none;
+ outline: 0;
+
+ &:hover {
+ color: darken($dropdown-title-btn-color, 15%);
+ }
+}
+
+.dropdown-menu-close {
+ right: 0;
+}
+
+.dropdown-menu-back {
+ left: 0;
+}
+
+.dropdown-input {
+ position: relative;
+ margin-bottom: 10px;
+
+ .fa {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ color: #c7c7c7;
+ font-size: 12px;
+ pointer-events: none;
+ }
+}
+
+.dropdown-input-field {
+ width: 100%;
+ padding: 0 7px;
+ color: $dropdown-input-color;
+ line-height: 30px;
+ border: 1px solid $dropdown-divider-color;
+ border-radius: 2px;
+ outline: 0;
+
+ &:focus {
+ color: $dropdown-link-color;
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+
+ + .fa {
+ color: $dropdown-link-color;
+ }
+ }
+
+ &:hover {
+ + .fa {
+ color: $dropdown-link-color;
+ }
+ }
+}
+
+.dropdown-content {
+ max-height: 215px;
+ overflow-y: scroll;
+}
+
+.dropdown-footer {
+ padding-top: 10px;
+ margin-top: 10px;
+ font-size: 13px;
+ border-top: 1px solid $dropdown-divider-color;
+}
+
+.dropdown-footer-list {
+ font-size: 14px;
+
+ a {
+ padding-left: 10px;
+ }
+}
+
+.dropdown-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ z-index: 9;
+ background-color: $dropdown-loading-bg;
+ font-size: 28px;
+
+ .fa {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -14px;
+ margin-left: -14px;
+ }
+}
+
+.dropdown-menu-labels {
+ .label {
+ position: relative;
+ width: 30px;
+ margin-right: 5px;
+ text-indent: -99999px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index cbfd4bc29b6..646e2610831 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -3,13 +3,11 @@
*
*/
.file-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
border: none;
- border-top: 1px solid #E7E9EE;
- border-bottom: 1px solid #E7E9EE;
+ border: 1px solid $border-color;
&.readme-holder {
+ margin-top: 10px;
border-bottom: 0;
}
@@ -32,12 +30,26 @@
right: 15px;
.btn {
- padding: 0px 10px;
+ padding: 0 10px;
font-size: 13px;
line-height: 28px;
}
}
+ .filename {
+ &.old {
+ span.idiff {
+ background-color: #f8cbcb;
+ }
+ }
+
+ &.new {
+ span.idiff {
+ background-color: #a6f3a6;
+ }
+ }
+ }
+
.left-options {
margin-top: -3px;
}
@@ -72,7 +84,7 @@
&.blob-no-preview {
background: #eee;
- text-shadow: 0 1px 2px #FFF;
+ text-shadow: 0 1px 2px #fff;
padding: 100px 0;
}
@@ -95,15 +107,6 @@
&:last-child {
border-right: none;
}
- background: #fff;
- }
- .lines {
- pre {
- padding: 0;
- margin: 0;
- background: none;
- border: none;
- }
}
img.avatar {
border: 0 none;
@@ -119,18 +122,18 @@
color: #888;
}
}
- td.blame-numbers {
- pre {
- color: #AAA;
- white-space: pre;
- }
- background: #f1f1f1;
- border-left: 1px solid #DDD;
+ td.line-numbers {
+ float: none;
+ border-left: 1px solid #ddd;
}
td.lines {
+ padding: 0;
code {
font-family: $monospace_font;
}
+ pre {
+ margin: 0;
+ }
}
}
@@ -155,7 +158,7 @@
}
&:hover {
- background: $hover;
+ background: $row-hover;
}
}
}
@@ -166,6 +169,19 @@
*/
&.code {
padding: 0;
+ -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
}
}
}
+
+span.idiff {
+ &.left {
+ border-top-left-radius: 2px;
+ border-bottom-left-radius: 2px;
+ }
+
+ &.right {
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 8e6922c9231..40a508c1ebc 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,30 +1,13 @@
.filter-item {
- margin-right: 15px;
+ margin-right: 6px;
+ vertical-align: top;
}
-@media (min-width: 800px) {
+@media (min-width: $screen-sm-min) {
.issues-filters,
.issues_bulk_update {
- select, .select2-container {
- width: 120px !important;
- display: inline-block;
+ .dropdown-menu-toggle {
+ width: 132px;
}
}
}
-
-@media (min-width: 1200px) {
- .issues-filters,
- .issues_bulk_update {
- select, .select2-container {
- width: 150px !important;
- display: inline-block;
- }
- }
-}
-
-.issues-filters,
-.issues_bulk_update {
- .select2-container .select2-choice {
- color: #444 !important;
- }
-}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 82eb50ad4be..1bfd0213995 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -8,10 +8,12 @@
.flash-notice {
@extend .alert;
@extend .alert-info;
+ margin: 0;
}
.flash-alert {
@extend .alert;
@extend .alert-danger;
+ margin: 0;
}
}
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
index e214567eca1..7a946109e3a 100644
--- a/app/assets/stylesheets/framework/fonts.scss
+++ b/app/assets/stylesheets/framework/fonts.scss
@@ -3,23 +3,39 @@
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
- src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf');
+ src:
+ local('Source Sans Pro Light'),
+ local('SourceSansPro-Light'),
+ font-url('SourceSansPro-Light.ttf.woff2') format('woff2'),
+ font-url('SourceSansPro-Light.ttf.woff') format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
- src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf');
+ src:
+ local('Source Sans Pro'),
+ local('SourceSansPro-Regular'),
+ font-url('SourceSansPro-Regular.ttf.woff2') format('woff2'),
+ font-url('SourceSansPro-Regular.ttf.woff') format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
- src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf');
+ src:
+ local('Source Sans Pro Semibold'),
+ local('SourceSansPro-Semibold'),
+ font-url('SourceSansPro-Semibold.ttf.woff2') format('woff2'),
+ font-url('SourceSansPro-Semibold.ttf.woff') format('woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
- src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf');
+ src:
+ local('Source Sans Pro Bold'),
+ local('SourceSansPro-Bold'),
+ font-url('SourceSansPro-Bold.ttf.woff2') format('woff2'),
+ font-url('SourceSansPro-Bold.ttf.woff') format('woff');
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 032d343df44..4cb4129b71b 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -2,16 +2,47 @@ textarea {
resize: vertical;
}
-input[type='search'].search-text-input {
- background-image: image-url("icon-search.png");
+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;
- padding-left: 25px;
+ 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;
+ background: #f2dede!important;
+ border-color: #d66;
text-shadow: 0 1px 1px #fff
}
@@ -38,6 +69,10 @@ label {
&.inline-label {
margin: 0;
}
+
+ &.label-light {
+ font-weight: 600;
+ }
}
.inline-input-group {
@@ -74,8 +109,11 @@ label {
.form-control {
@include box-shadow(none);
- height: 42px;
- padding: 8px $gl-padding;
+ border-radius: 3px;
+}
+
+.form-control-inline {
+ display: inline;
}
.wiki-content {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 8d9a0aae568..2a4cf4fc335 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -23,13 +23,13 @@
&:hover {
background-color: $color-darker;
a {
- color: #FFF;
+ color: #fff;
}
}
}
.collapse-nav a {
- color: #FFF;
+ color: #fff;
background: $color;
}
@@ -42,7 +42,7 @@
&:hover {
background-color: $color-dark;
- color: #FFF;
+ color: #fff;
text-decoration: none;
}
}
@@ -71,7 +71,7 @@
}
&.active a {
- color: #FFF;
+ color: #fff;
background: $color-dark;
&.no-highlight {
@@ -79,42 +79,42 @@
}
i {
- color: #FFF
+ color: #fff
}
}
}
}
}
-$theme-blue: #2980B9;
+$theme-blue: #2980b9;
$theme-charcoal: #333c47;
-$theme-graphite: #888888;
+$theme-graphite: #888;
$theme-gray: #373737;
$theme-green: #019875;
-$theme-violet: #554488;
+$theme-violet: #548;
body {
&.ui_blue {
- @include gitlab-theme(#BECDE9, $theme-blue, #1970A9, #096099);
+ @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099);
}
&.ui_charcoal {
- @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272D);
+ @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d);
}
&.ui_graphite {
- @include gitlab-theme(#CCCCCC, $theme-graphite, #777777, #666666);
+ @include gitlab-theme(#ccc, $theme-graphite, #777, #666);
}
&.ui_gray {
- @include gitlab-theme(#979797, $theme-gray, #272727, #222222);
+ @include gitlab-theme(#979797, $theme-gray, #272727, #222);
}
&.ui_green {
- @include gitlab-theme(#AADDCC, $theme-green, #018865, #017855);
+ @include gitlab-theme(#adc, $theme-green, #018865, #017855);
}
&.ui_violet {
- @include gitlab-theme(#9988CC, $theme-violet, #443366, #332255);
+ @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 4dbbb56104b..71a7ecab8ef 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -7,8 +7,8 @@ header {
&.navbar-empty {
height: 58px;
- background: #FFF;
- border-bottom: 1px solid #EEE;
+ background: #fff;
+ border-bottom: 1px solid #eee;
.center-logo {
margin: 11px 0;
@@ -28,6 +28,7 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
+ border-bottom: 1px solid #eee;
.container-fluid {
width: 100% !important;
@@ -46,7 +47,7 @@ header {
text-align: center;
&:hover, &:focus, &:active {
- background-color: #FFF;
+ background-color: #fff;
}
}
@@ -58,7 +59,7 @@ header {
right: 2px;
&:hover {
- background-color: #EEE;
+ background-color: #eee;
}
&.active {
color: #7f8fa4;
@@ -72,11 +73,11 @@ header {
.title {
margin: 0;
- overflow: hidden;
font-size: 19px;
line-height: $header-height;
font-weight: normal;
color: #4c4e54;
+ overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
@@ -87,6 +88,22 @@ header {
text-decoration: underline;
}
}
+
+ .dropdown-toggle-caret {
+ position: relative;
+ top: -2px;
+ width: 12px;
+ line-height: 12px;
+ margin-left: 5px;
+ font-size: 10px;
+ text-align: center;
+ cursor: pointer;
+ }
+
+ .project-item-select {
+ right: auto;
+ left: 0;
+ }
}
.navbar-collapse {
@@ -107,16 +124,10 @@ header {
.search-input {
width: 220px;
- background-image: image-url("icon-search.png");
- background-repeat: no-repeat;
- background-position: 195px;
- @include input-big;
&:focus {
@include box-shadow(none);
outline: none;
- border-color: #DDD;
- background-color: #FFF;
}
}
}
@@ -130,18 +141,18 @@ header {
margin-left: $sidebar_collapsed_width;
}
-@media (max-width: $screen-md-max) {
- .header-collapsed, .header-expanded {
+.header-collapsed {
+ margin-left: $sidebar_collapsed_width;
+
+ @media (min-width: $screen-md-min) {
@include collapsed-header;
}
}
-@media(min-width: $screen-md-max) {
- .header-collapsed {
- @include collapsed-header;
- }
+.header-expanded {
+ margin-left: $sidebar_collapsed_width;
- .header-expanded {
+ @media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
}
@@ -151,7 +162,7 @@ header {
font-size: 18px;
.navbar-nav {
- margin: 0px;
+ margin: 0;
float: none !important;
.visible-xs, .visable-sm {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 2e13ee842e0..7cf4d4fba42 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -1,8 +1,8 @@
.file-content.code {
border: none;
box-shadow: none;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
table-layout: fixed;
pre {
@@ -17,6 +17,7 @@
overflow-y: hidden;
white-space: pre;
word-wrap: normal;
+ border-left: 1px solid;
code {
font-family: $monospace_font;
@@ -25,7 +26,7 @@
padding: 0;
.line {
- display: inline;
+ display: inline-block;
}
}
}
@@ -43,8 +44,10 @@
white-space: nowrap;
i {
+ float: left;
+ margin-top: 3px;
+ margin-right: 5px;
visibility: hidden;
- @extend .pull-left;
}
&:hover i {
@@ -53,18 +56,3 @@
}
}
}
-
-.note-text .code {
- border: none;
- box-shadow: none;
- background: $background-color;
- padding: 1em;
- overflow-x: auto;
-
- code {
- font-family: $monospace_font;
- white-space: pre;
- word-wrap: normal;
- padding: 0;
- }
-}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index e93dbab0c42..7f7b7c806e7 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -5,32 +5,38 @@
*/
.status-box {
- @include border-radius(3px);
+
+ /* Extra small devices (phones, less than 768px) */
+ /* No media query since this is the default in Bootstrap */
+ padding: 5px 11px;
+ margin-top: 4px;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ padding: 0 $gl-btn-padding;
+ margin-top: 5px;
+ }
+ @include border-radius(3px);
display: block;
float: left;
- padding: 0 $gl-padding;
- font-weight: normal;
margin-right: 10px;
+ color: #fff;
font-size: $gl-font-size;
+ line-height: 25px;
&.status-box-closed {
background-color: $gl-danger;
- color: #FFF;
}
&.status-box-merged {
background-color: $gl-primary;
- color: #FFF;
}
&.status-box-open {
background-color: $green-light;
- color: #FFF;
}
&.status-box-expired {
background: #cea61b;
- color: #FFF;
}
}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 871b808bad4..525ed81b059 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -3,13 +3,13 @@
font-size: $font-size-base;
&.ui-datepicker-inline {
- border: 1px solid #DDD;
+ border: 1px solid #ddd;
padding: 10px;
width: 270px;
.ui-datepicker-header {
- background: #FFF;
- border-color: #DDD;
+ background: #fff;
+ border-color: #ddd;
}
.ui-datepicker-calendar td a {
@@ -19,7 +19,7 @@
}
&.ui-autocomplete {
- border-color: #DDD;
+ border-color: #ddd;
padding: 0;
margin-top: 2px;
z-index: 1001;
@@ -30,26 +30,37 @@
}
.ui-state-default {
- border: 1px solid #FFF;
- background: #FFF;
+ border: 1px solid #fff;
+ background: #fff;
color: #777;
}
.ui-state-highlight {
- border: 1px solid #EEE;
- background: #EEE;
+ border: 1px solid #eee;
+ background: #eee;
}
.ui-state-active {
border: 1px solid $gl-primary;
background: $gl-primary;
- color: #FFF;
+ color: #fff;
}
.ui-state-hover,
.ui-state-focus {
- border: 1px solid $hover;
- background: $hover;
+ border: 1px solid $row-hover;
+ background: $row-hover;
color: #333;
}
}
+
+.ui-sortable-handle {
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+
+ &:active {
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ }
+}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index a1a9990241d..e901c78d02f 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -5,8 +5,6 @@ html {
}
body {
- background-color: #F3F3F3 !important;
-
&.navless {
background-color: white !important;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 1c74e525a60..2b4bb1eebf9 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -3,6 +3,7 @@
*
*/
.well-list {
+ position: relative;
margin: 0;
padding: 0;
list-style: none;
@@ -38,7 +39,7 @@
&.smoke { background-color: $background-color; }
&:hover {
- background: $hover;
+ background: $row-hover;
}
&:last-child {
@@ -74,7 +75,7 @@
/** light list with border-bottom between li **/
-ul.bordered-list {
+ul.bordered-list, ul.unstyled-list {
@include basic-list;
&.top-list {
@@ -88,6 +89,10 @@ ul.bordered-list {
}
}
+ul.unstyled-list > li {
+ border-bottom: none;
+}
+
ul.task-list {
li.task-list-item {
list-style-type: none;
@@ -105,11 +110,21 @@ ul.content-list {
padding: 0;
> li {
- padding: $gl-padding;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- color: $gl-gray;
+ color: $list-text-color;
+ font-size: $list-font-size;
+
+ .title {
+ color: $list-title-color;
+ font-weight: 600;
+ }
+
+ .description {
+ p {
+ @include str-truncated;
+ margin-bottom: 0;
+ }
+ }
.avatar {
margin-right: 15px;
@@ -126,10 +141,8 @@ ul.content-list {
}
}
-.panel > .content-list {
- li {
- margin: 0;
- }
+.panel > .content-list > li {
+ padding: $gl-padding-top $gl-padding;
}
ul.controls {
@@ -144,7 +157,7 @@ ul.controls {
> li {
float: left;
margin-right: 10px;
-
+
&:last-child {
margin-right: 0;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 4a00a197d9a..8328aac4e7a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -65,48 +65,25 @@
position: relative;
}
-.md-header {
- ul {
- float: left;
- margin-bottom: 1px;
- }
-}
-
.referenced-users {
color: #4c4e54;
padding-top: 10px;
}
.md-preview-holder {
- background: #FFF;
+ background: #fff;
border: 1px solid #ddd;
min-height: 169px;
padding: 5px;
box-shadow: none;
}
-.new_note,
-.edit_note,
-.detail-page-description,
-.milestone-description,
-.wiki-content,
-.merge-request-form {
- .nav-tabs {
- margin-bottom: 0;
- border: none;
-
- li a,
- li.active a {
- border: 1px solid #DDD;
- }
- }
-}
-
.markdown-area {
@include border-radius(0);
- background: #FFF;
+ background: #fff;
border: 1px solid #ddd;
min-height: 140px;
+ max-height: 500px;
padding: 5px;
box-shadow: none;
width: 100%;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 41fd890f14f..377bfa174bd 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -67,17 +67,17 @@
* Base mixin for lists in GitLab
*/
@mixin basic-list {
- margin: 5px 0px;
- padding: 0px;
+ margin: 5px 0;
+ padding: 0;
list-style: none;
> li {
@include clearfix;
padding: 10px 0;
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eee;
display: block;
- margin: 0px;
+ margin: 0;
&:last-child {
border-bottom: none;
@@ -118,38 +118,3 @@
font-size: 16px;
line-height: 24px;
}
-
-@mixin nav-menu {
- padding: 0;
- margin: 0;
- list-style: none;
- height: 56px;
-
- li {
- display: inline-block;
-
- a {
- padding: 14px;
- font-size: 15px;
- line-height: 28px;
- color: #959494;
- border-bottom: 2px solid transparent;
-
- &:hover, &:active, &:focus {
- text-decoration: none;
- outline: none;
- }
- }
-
- &.active a {
- color: #616060;
- border-bottom: 2px solid #4688f1;
- }
-
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: #78a;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index c00709fb6bb..5ea4f9a49db 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -9,7 +9,7 @@
padding-right: 5px;
}
- .nav.nav-tabs > li > a {
+ .nav-links > li > a {
padding: 10px;
font-size: 12px;
margin-right: 3px;
@@ -81,7 +81,7 @@
display: none;
}
- .center-top-menu, .left-top-menu {
+ .nav-links, .nav-links {
li a {
font-size: 14px;
padding: 19px 10px;
@@ -100,11 +100,6 @@
}
@media (max-width: $screen-sm-max) {
- .page-with-sidebar .content-wrapper {
- padding: 0;
- padding-top: 1px;
- }
-
.issues-filters {
.milestone-filter, .labels-filter {
display: none;
@@ -121,7 +116,7 @@
display: none;
}
- aside {
+ aside:not(.right-sidebar){
display: none;
}
@@ -133,12 +128,12 @@
.show-aside {
display: none;
position: fixed;
- right: 0px;
+ right: 0;
top: 30%;
padding: 5px 15px;
- background: #EEE;
+ background: #eee;
font-size: 20px;
color: #777;
z-index: 100;
- @include box-shadow(0 1px 2px #DDD);
+ @include box-shadow(0 1px 2px #ddd);
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
new file mode 100644
index 00000000000..5f4ce87b085
--- /dev/null
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -0,0 +1,142 @@
+.nav-links {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ height: auto;
+ border-bottom: 1px solid $border-color;
+
+ li {
+ display: inline-block;
+
+ a {
+ display: inline-block;
+ padding: 14px;
+ padding-top: $gl-padding;
+ padding-bottom: 11px;
+ margin-bottom: -1px;
+ font-size: 15px;
+ line-height: 28px;
+ color: #959494;
+ border-bottom: 2px solid transparent;
+
+ &:hover, &:active, &:focus {
+ text-decoration: none;
+ outline: none;
+ }
+ }
+
+ &.active a {
+ color: #000;
+ border-bottom: 2px solid #4688f1;
+ }
+
+ .badge {
+ font-weight: normal;
+ background-color: #eee;
+ color: #78a;
+ }
+ }
+}
+
+.top-area {
+ @include clearfix;
+
+ border-bottom: 1px solid #eee;
+
+ .nav-text {
+ padding-top: 16px;
+ padding-bottom: 11px;
+ display: inline-block;
+ width: 50%;
+ line-height: 28px;
+
+ /* Small devices (phones, tablets, 768px and lower) */
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
+ }
+
+ .nav-links {
+ display: inline-block;
+ width: 50%;
+ margin-bottom: 0;
+ border-bottom: none;
+
+ /* Small devices (phones, tablets, 768px and lower) */
+ @media (max-width: $screen-sm-max) {
+ width: 100%;
+ }
+ }
+
+ .nav-controls {
+ width: 50%;
+ display: inline-block;
+ float: right;
+ text-align: right;
+ padding: 11px 0;
+ margin-bottom: 0;
+
+ > .dropdown {
+ margin-right: $gl-padding-top;
+ display: inline-block;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ > .btn {
+ margin-right: $gl-padding-top;
+ display: inline-block;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ > .btn-grouped {
+ float: none;
+ }
+
+ > form {
+ display: inline-block;
+ }
+
+ input {
+ height: 34px;
+ display: inline-block;
+ position: relative;
+ top: 1px;
+ margin-right: $gl-padding-top;
+
+ /* Medium devices (desktops, 992px and up) */
+ @media (min-width: $screen-md-min) { width: 200px; }
+
+ /* Large devices (large desktops, 1200px and up) */
+ @media (min-width: $screen-lg-min) { width: 250px; }
+
+ &.input-short {
+ /* Medium devices (desktops, 992px and up) */
+ @media (min-width: $screen-md-min) { width: 170px; }
+
+ /* Large devices (large desktops, 1200px and up) */
+ @media (min-width: $screen-lg-min) { width: 210px; }
+ }
+ }
+
+ /* Hide on extra small devices (phones) */
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+
+ /* Small devices (tablets, 768px and lower) */
+ @media (max-width: $screen-sm-max) {
+ width: 100%;
+ text-align: left;
+
+ input {
+ width: 300px;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index 2cd30491bf5..b6f21fd8c91 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -1,35 +1,11 @@
.gl-pagination {
+ text-align: center;
border-top: 1px solid $border-color;
- background-color: $background-color;
- margin: -$gl-padding;
+ margin: 0;
margin-top: 0;
.pagination {
padding: 0;
- margin: 0;
- display: block;
-
- li.first,
- li.last,
- li.next,
- li.prev {
- > a {
- color: $link-color;
-
- &:hover {
- color: #fff;
- }
- }
- }
-
- li > a,
- li > span {
- border: none;
- margin: 0;
- @include border-radius(0 !important);
- padding: 13px 19px;
- border-right: 1px solid $border-color;
- }
}
}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index 57b9451b264..ae7bdf14c40 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -2,7 +2,13 @@
margin-bottom: $gl-padding;
.panel-heading {
- padding: 7px $gl-padding;
+ padding: $gl-vert-padding $gl-padding;
+ line-height: 36px;
+
+ .controls {
+ margin-top: -2px;
+ float: right;
+ }
}
.panel-body {
@@ -14,7 +20,3 @@
}
}
}
-
-.container-blank .panel .panel-heading {
- line-height: 42px !important;
-}
diff --git a/app/assets/stylesheets/framework/progress.scss b/app/assets/stylesheets/framework/progress.scss
new file mode 100644
index 00000000000..e9800bd24b5
--- /dev/null
+++ b/app/assets/stylesheets/framework/progress.scss
@@ -0,0 +1,5 @@
+html.turbolinks-progress-bar::before {
+ background-color: $progress-color!important;
+ height: 2px!important;
+ box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color;
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index af145191bc8..b3371229d5a 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -1,49 +1,53 @@
/** Select2 selectbox style override **/
+.select2-container {
+ width: 100% !important;
+}
+
.select2-container, .select2-container.select2-drop-above {
.select2-choice {
- background: #FFF;
- border-color: #DDD;
- height: 42px;
- padding: 8px $gl-padding;
+ background: #fff;
+ border-color: $input-border;
+ border-color: $border-white-light;
+ height: 35px;
+ padding: $gl-vert-padding $gl-btn-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
- @include border-radius(2px);
+ @include border-radius($border-radius-default);
.select2-arrow {
- background: #FFF;
- border-left: none;
- padding-top: 5px;
+ background-image: none;
+ background-color: transparent;
+ border: none;
+ padding-top: 6px;
+ padding-right: 10px;
+
+ b {
+ @extend .caret;
+ color: $gray-darkest;
+ }
}
.select2-chosen {
- color: $gl-text-color;
+ margin-right: 15px;
}
- &.select2-default {
- .select2-chosen {
- color: #999;
- }
+ &:hover {
+ background-color: $gray-dark;
+ border-color: $border-white-normal;
+ color: $gl-text-color;
}
}
}
-.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice{
- color: #7f8fa4;
- border: 1px solid #e7e9ed;
-}
-
-
.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 border-radius (0px);
-
- padding: 16px;
- border: none !important;
+ @include border-radius ($border-radius-default);
+ border: none;
}
.select2-results .select2-result-label {
- padding: 9px;
+ padding: 10px 15px;
}
.select2-drop{
@@ -56,15 +60,30 @@
.select2-results li.select2-result-with-children > .select2-result-label {
font-weight: 600;
- color: #313236;
+ color: $gl-text-color;
+}
+
+.select2-container-active {
+ .select2-choice, .select2-choices {
+ @include box-shadow(none);
+ }
+}
+
+.select2-dropdown-open {
+ .select2-choice {
+ border-color: $border-white-normal;
+ outline: 0;
+ background-image: none;
+ background-color: $white-dark;
+ @include box-shadow($gl-btn-active-gradient);
+ }
}
.select2-container-multi {
.select2-choices {
- @include border-radius(2px);
+ @include border-radius($border-radius-default);
border-color: $input-border;
- background: white;
- padding-left: $gl-padding / 2;
+ background: none;
.select2-search-field input {
padding: $gl-padding / 2;
@@ -76,14 +95,16 @@
.select2-search-choice {
margin: 8px 0 0 8px;
- background: white;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
+ background-color: $background-color;
+ background-image: none;
.select2-search-choice-close {
- top: 5px;
+ top: 4px;
+ left: 3px;
}
&.select2-search-choice-focus {
@@ -91,22 +112,25 @@
}
}
}
+
+ &.select2-container-active .select2-choices,
+ &.select2-dropdown-open .select2-choices {
+ border-color: $border-white-normal;
+ @include box-shadow($gl-btn-active-gradient);
+ }
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
}
.select2-drop-active {
- border: 1px solid #BBB !important;
- margin-top: 4px;
- font-size: 13px;
+ margin-top: 6px;
+ font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
- .select2-search input {
- background: #fafafa;
- border-color: #DDD;
- }
-
.select2-results {
max-height: 350px;
.select2-highlighted {
@@ -115,8 +139,34 @@
}
}
-.select2-container {
- width: 100% !important;
+.select2-search {
+ padding: 15px 15px 5px;
+
+ .select2-drop-auto-width & {
+ padding: 15px 15px 5px;
+ }
+}
+
+.select2-search input {
+ padding: 2px 25px 2px 5px;
+ background: #fff image-url('select2.png');
+ background-repeat: no-repeat;
+ background-position: right 0 bottom 6px;
+ border: 1px solid $input-border;
+ @include border-radius($border-radius-default);
+ @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
+
+ &:focus {
+ border-color: $input-border-focus;
+ }
+}
+
+.select2-search input.select2-active {
+ background-color: #fff;
+ background-image: image-url('select2-spinner.gif') !important;
+ background-repeat: no-repeat;
+ background-position: right 5px center !important;
+ background-size: 16px 16px !important;
}
/** Branch/tag selector **/
@@ -124,10 +174,19 @@
width: 160px !important;
}
-.ajax-users-dropdown, .ajax-project-users-dropdown {
- .select2-search {
- padding-top: 2px;
- }
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-ajax-error,
+.select2-results .select2-selection-limit {
+ background: $gray-light;
+ display: list-item;
+ padding: 10px 15px;
+}
+
+
+.select2-results {
+ margin: 0;
+ padding: 10px 0;
}
.ajax-users-select {
@@ -170,7 +229,7 @@
.namespace-result {
.namespace-kind {
- color: #AAA;
+ color: #aaa;
font-weight: normal;
}
.namespace-path {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 458af76cb75..be05db58c40 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -12,20 +12,43 @@
height: 100%;
transition-duration: .3s;
}
+
+ .gitlab-text-container-link {
+ z-index: 1;
+ 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: 99;
+ z-index: 999;
background: $background-color;
}
.content-wrapper {
width: 100%;
- padding: 20px;
.container-fluid {
- background: #FFF;
- padding: $gl-padding;
+ background: #fff;
+ padding: 0 $gl-padding;
&.container-blank {
background: none;
@@ -71,7 +94,7 @@
width: 158px;
float: left;
margin: 0;
- margin-left: 14px;
+ margin-left: 50px;
font-size: 19px;
line-height: 41px;
font-weight: normal;
@@ -80,7 +103,7 @@
}
&:hover {
- background-color: #EEE;
+ background-color: #eee;
}
}
@@ -105,7 +128,7 @@
.tanuki-shape {
transition: all 0.8s;
- &:hover {
+ &:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
}
@@ -120,7 +143,7 @@
overflow: hidden;
&.navbar-collapse {
- padding: 0px !important;
+ padding: 0 !important;
}
li {
@@ -159,7 +182,7 @@
.count {
float: right;
background: #eee;
- padding: 0px 8px;
+ padding: 0 8px;
@include border-radius(6px);
}
@@ -171,8 +194,8 @@
}
.sidebar-subnav {
- margin-left: 0px;
- padding-left: 0px;
+ margin-left: 0;
+ padding-left: 0;
li {
list-style: none;
@@ -180,7 +203,20 @@
}
@mixin expanded-sidebar {
- padding-left: $sidebar_width;
+ padding-left: $sidebar_collapsed_width;
+
+ @media (min-width: $screen-md-min) {
+ padding-left: $sidebar_width;
+ }
+
+ &.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;
+ }
+ }
.sidebar-wrapper {
width: $sidebar_width;
@@ -204,6 +240,15 @@
@mixin collapsed-sidebar {
padding-left: $sidebar_collapsed_width;
+ &.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;
+ }
+ }
+
.sidebar-wrapper {
width: $sidebar_collapsed_width;
@@ -267,26 +312,16 @@
background: #f2f6f7;
}
-@media (max-width: $screen-md-max) {
- .page-sidebar-collapsed {
- @include collapsed-sidebar;
- }
-
- .page-sidebar-expanded {
+.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;
}
-
- .collapse-nav {
- display: none;
- }
}
-@media(min-width: $screen-md-max) {
- .page-sidebar-collapsed {
- @include collapsed-sidebar;
- }
-
- .page-sidebar-expanded {
- @include expanded-sidebar;
- }
+.page-sidebar-expanded {
+ @include expanded-sidebar;
}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 793ab3d9bb9..75b770ae5a2 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -1,13 +1,11 @@
.table-holder {
- margin: -$gl-padding;
- margin-top: 0;
- margin-bottom: 0;
+ margin: 0;
}
table {
&.table {
margin-bottom: $gl-padding;
-
+
.dropdown-menu a {
text-decoration: none;
}
@@ -32,14 +30,15 @@ table {
}
th {
+ background-color: $background-color;
font-weight: normal;
font-size: 15px;
- border-bottom: 1px solid $border-color !important;
+ border-bottom: 1px solid $border-color;
}
td {
- border-color: $table-border-color !important;
- border-bottom: 1px solid;
+ border-color: $table-border-color;
+ border-bottom: 1px solid $border-color;
}
}
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index ff41e26ed8a..aa244fe548d 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -5,15 +5,13 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding;
+ padding: $gl-padding $gl-btn-padding;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
&:target {
- background: $hover;
+ background: $row-hover;
}
&:last-child {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 94f0ed761df..dd42db1840f 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -22,7 +22,7 @@
// Components
@import "bootstrap/component-animations";
-@import "bootstrap/dropdowns";
+// @import "bootstrap/dropdowns";
@import "bootstrap/button-groups";
@import "bootstrap/input-groups";
@import "bootstrap/navs";
@@ -95,51 +95,10 @@
}
&.label-inverse {
- background-color: #333333;
+ background-color: #333;
}
}
-// Nav tabs
-.nav.nav-tabs {
- margin-bottom: 15px;
-
- li {
- > a {
- margin-right: 5px;
- line-height: 20px;
- border-color: #EEE;
- color: #888;
- border-bottom: 1px solid #ddd;
- .badge {
- background-color: #eee;
- color: #888;
- text-shadow: 0 1px 1px #fff;
- }
- i.fa {
- line-height: 14px;
- }
- }
- &.active {
- > a {
- border-color: #CCC;
- border-bottom: 1px solid #fff;
- color: #333;
- font-weight: bold;
- }
- }
- }
-}
-
-.nav-tabs > li > a,
-.nav-pills > li > a {
- color: #666;
-}
-
-.nav-pills > .active > a > span > .badge {
- background-color: #fff;
- color: $gl-primary;
-}
-
/**
* fix to keep tooltips position in top navigation bar
@@ -155,22 +114,9 @@
*
*/
-.container-blank .panel .panel-heading {
- font-size: 17px;
- line-height: 38px;
-}
-
.panel {
box-shadow: none;
- .panel-heading {
- .panel-head-actions {
- position: relative;
- top: -5px;
- float: right;
- }
- }
-
.panel-body {
form, pre {
margin: 0;
@@ -192,7 +138,7 @@
}
.btn-clipboard {
- min-width: 0px;
+ min-width: 0;
}
}
@@ -221,12 +167,6 @@
}
}
-.alert-help {
- background-color: $background-color;
- border: 1px solid $border-color;
- color: $gl-gray;
-}
-
// Typography =================================================================
.text-primary,
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 63868a34e2a..f63ac033234 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -22,9 +22,9 @@ $brand-info: $gl-info;
$brand-warning: $gl-warning;
$brand-danger: $gl-danger;
-$border-radius-base: 2px !default;
-$border-radius-large: 2px !default;
-$border-radius-small: 2px !default;
+$border-radius-base: 3px !default;
+$border-radius-large: 3px !default;
+$border-radius-small: 3px !default;
//== Scaffolding
@@ -46,7 +46,7 @@ $font-size-base: $gl-font-size;
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-$padding-base-vertical: 9px;
+$padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding;
$component-active-color: #fff;
$component-active-bg: $brand-info;
@@ -57,7 +57,7 @@ $component-active-bg: $brand-info;
$input-color: $text-color;
$input-border: #e7e9ed;
-$input-border-focus: #7F8FA4;
+$input-border-focus: #7f8fa4;
$legend-color: $text-color;
@@ -66,20 +66,20 @@ $legend-color: $text-color;
//##
$pagination-color: $gl-gray;
-$pagination-bg: $background-color;
-$pagination-border: transparent;
+$pagination-bg: #fff;
+$pagination-border: $border-color;
-$pagination-hover-color: #fff;
-$pagination-hover-bg: $brand-info;
-$pagination-hover-border: transparent;
+$pagination-hover-color: $gl-gray;
+$pagination-hover-bg: $row-hover;
+$pagination-hover-border: $border-color;
-$pagination-active-color: #fff;
-$pagination-active-bg: $brand-info;
-$pagination-active-border: transparent;
+$pagination-active-color: $blue-dark;
+$pagination-active-bg: #fff;
+$pagination-active-border: $border-color;
-$pagination-disabled-color: #fff;
-$pagination-disabled-bg: lighten($brand-info, 15%);
-$pagination-disabled-border: transparent;
+$pagination-disabled-color: #cdcdcd;
+$pagination-disabled-bg: $background-color;
+$pagination-disabled-border: $border-color;
//== Form states and alerts
@@ -125,8 +125,8 @@ $panel-inner-border: $border-color;
//
//##
-$well-bg: #F9F9F9;
-$well-border: #EEE;
+$well-bg: #f9f9f9;
+$well-border: #eee;
//== Code
//
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index c3e4ad0ad00..949295a1d0c 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -27,13 +27,13 @@
line-height: 10px;
color: #555;
vertical-align: middle;
- background-color: #FCFCFC;
+ background-color: #fcfcfc;
border-width: 1px;
border-style: solid;
- border-color: #CCC #CCC #BBB;
+ border-color: #ccc #ccc #bbb;
border-image: none;
border-radius: 3px;
- box-shadow: 0px -1px 0px #BBB inset;
+ box-shadow: 0 -1px 0 #bbb inset;
}
h1 {
@@ -54,17 +54,17 @@
h3 {
margin: 24px 0 12px 0;
- font-size: 1.25em;
+ font-size: 1.1em;
}
h4 {
margin: 24px 0 12px 0;
- font-size: 1.1em;
+ font-size: 0.98em;
}
h5 {
margin: 24px 0 12px 0;
- font-size: 1em;
+ font-size: 0.95em;
}
h6 {
@@ -87,8 +87,8 @@
}
p {
- color:#5c5d5e;
- margin:6px 0 0 0;
+ color: #5c5d5e;
+ margin: 6px 0 0 0;
}
table {
@@ -102,11 +102,10 @@
}
pre {
- margin: 12px 0 12px 0 !important;
- background-color: #f8fafc;
- font-size: 13px !important;
- color: #5b6169;
- line-height: 1.6em !important;
+ margin: 12px 0 12px 0;
+ font-size: 13px;
+ line-height: 1.6em;
+ overflow-x: auto;
@include border-radius(2px);
}
@@ -116,7 +115,7 @@
ul, ol {
padding: 0;
- margin: 6px 0 6px 18px !important;
+ margin: 6px 0 6px 28px !important;
}
li {
@@ -150,13 +149,13 @@
}
&:hover > a.anchor {
- $size: 16px;
+ $size: 14px;
position: absolute;
right: 100%;
top: 50%;
- margin-top: -$size/2;
- margin-right: 0px;
- padding-right: 20px;
+ margin-top: -11px;
+ margin-right: 0;
+ padding-right: 15px;
display: inline-block;
width: $size;
height: $size;
@@ -177,7 +176,7 @@ body {
}
.page-title {
- margin-top: 0px;
+ margin-top: $gl-padding;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
@@ -188,7 +187,7 @@ body {
}
.page-title-empty {
- margin-top: 0px;
+ margin-top: 0;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
@@ -197,18 +196,13 @@ body {
h1, h2, h3, h4, h5, h6 {
color: $gl-header-color;
- font-weight: 500;
+ font-weight: 600;
}
/** CODE **/
pre {
font-family: $monospace_font;
- &.dark {
- background: #333;
- color: $background-color;
- }
-
&.plain-readme {
background: none;
border: none;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index af75123b0af..211ead7319d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,17 +1,20 @@
-$hover: #faf9f9;
-$gl-text-color: #54565B;
-$gl-text-green: #4A2;
-$gl-text-red: #D12F19;
-$gl-text-orange: #D90;
+$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;
-$nprogress-color: #c0392b;
+$progress-color: #c0392b;
$gl-font-size: 15px;
$list-font-size: 15px;
$sidebar_collapsed_width: 62px;
$sidebar_width: 230px;
+$gutter_collapsed_width: 62px;
+$gutter_width: 290px;
+$gutter_inner_width: 258px;
$avatar_radius: 50%;
$code_font_size: 13px;
$code_line_height: 1.5;
@@ -22,64 +25,87 @@ $header-height: 58px;
$fixed-layout-width: 1280px;
$gl-gray: #5a5a5a;
$gl-padding: 16px;
-$gl-padding-top:10px;
-$gl-avatar-size: 46px;
+$gl-btn-padding: 10px;
+$gl-vert-padding: 6px;
+$gl-padding-top: 10px;
+$gl-avatar-size: 40px;
+$secondary-text: #7f8fa4;
+$error-exclamation-point: #e62958;
+$border-radius-default: 3px;
+$list-title-color: #333;
+$list-text-color: #555;
+
+$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;
/*
* Color schema
*/
-$white-light: #FFFFFF;
+$white-light: #fff;
$white-normal: #ededed;
$white-dark: #ededed;
-$gray-light: #f7f7f7;
-$gray-normal: #ededed;
+$gray-light: #faf9f9;
+$gray-normal: #f5f5f5;
$gray-dark: #ededed;
+$gray-darkest: #c9c9c9;
-$green-light: #31AF64;
-$green-normal: #2FAA60;
-$green-dark: #2CA05B;
+$green-light: #38ae67;
+$green-normal: #2faa60;
+$green-dark: #2ca05b;
-$blue-light: #2EA8E5;
-$blue-normal: #2D9FD8;
-$blue-dark: #2897CE;
+$blue-light: #2ea8e5;
+$blue-normal: #2d9fd8;
+$blue-dark: #2897ce;
-$blue-medium-light: #3498CB;
-$blue-medium: #2F8EBF;
-$blue-medium-dark: #2D86B4;
+$blue-medium-light: #3498cb;
+$blue-medium: #2f8ebf;
+$blue-medium-dark: #2d86b4;
-$orange-light: #FC6443;
-$orange-normal: #E75E40;
-$orange-dark: #CE5237;
+$orange-light: rgba(252, 109, 38, 0.80);
+$orange-normal: #e75e40;
+$orange-dark: #ce5237;
-$red-light: #F43263;
-$red-normal: #E52C5A;
-$red-dark: #D22852;
+$red-light: #f06559;
+$red-normal: #e52c5a;
+$red-dark: #d22852;
-$border-white-light: #F1F2F4;
-$border-white-normal: #D6DAE2;
-$border-white-dark: #C6CACF;
+$border-white-light: #f1f2f4;
+$border-white-normal: #d6dae2;
+$border-white-dark: #c6cacf;
-$border-gray-light: #d1d1d1;
-$border-gray-normal: #D6DAE2;
-$border-gray-dark: #C6CACF;
+$border-gray-light: rgba(0, 0, 0, 0.06);
+$border-gray-normal: rgba(0, 0, 0, 0.10);;
+$border-gray-dark: #c6cacf;
-$border-green-light: #2FAA60;
-$border-green-normal: #2CA05B;
+$border-green-light: #2faa60;
+$border-green-normal: #2ca05b;
$border-green-dark: #279654;
-$border-blue-light: #2D9FD8;
-$border-blue-normal: #2897CE;
-$border-blue-dark: #258DC1;
+$border-blue-light: #2d9fd8;
+$border-blue-normal: #2897ce;
+$border-blue-dark: #258dc1;
+
+$border-orange-light: #fc6d26;
+$border-orange-normal: #ce5237;
+$border-orange-dark: #c14e35;
+
+$border-red-light: #f24f41;
+$border-red-normal: #d22852;
+$border-red-dark: #ca264f;
-$border-orange-light: #ED5C3D;
-$border-orange-normal: #CE5237;
-$border-orange-dark: #C14E35;
+$help-well-bg: #fafafa;
+$help-well-border: #e5e5e5;
-$border-red-light: #E52C5A;
-$border-red-normal: #D22852;
-$border-red-dark: #CA264F;
+$warning-message-bg: #fbf2d9;
+$warning-message-color: #9e8e60;
+$warning-message-border: #f0e2bb;
/* header */
$light-grey-header: #faf9f9;
@@ -92,6 +118,8 @@ $gl-success: $green-normal;
$gl-info: $blue-normal;
$gl-warning: $orange-normal;
$gl-danger: $red-normal;
+$gl-btn-active-background: rgba(0, 0, 0, 0.12);
+$gl-btn-active-gradient: inset 0 0 4px $gl-btn-active-background;
/*
* Commit Diff Colors
@@ -104,3 +132,33 @@ $deleted: #f77;
*/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+/*
+* Dropdowns
+*/
+$dropdown-bg: #fff;
+$dropdown-link-color: #555;
+$dropdown-link-hover-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-loading-bg: rgba(#fff, .6);
+
+$dropdown-toggle-bg: #fff;
+$dropdown-toggle-color: #626262;
+$dropdown-toggle-border-color: #eaeaea;
+$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
+$dropdown-toggle-icon-color: #c4c4c4;
+$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
+
+/*
+ * Award emoji
+ */
+$award-emoji-menu-bg: #fff;
+$award-emoji-menu-border: #f1f2f4;
+$award-emoji-new-btn-icon-color: #dcdcdc;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 32e2c020e06..02e24ec7c4d 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -1,17 +1,13 @@
.zennable {
- .zen-toggle-comment {
- display: none;
- }
-
- .zen-enter-link {
+ a.js-zen-enter {
color: $gl-gray;
position: absolute;
- top: 0px;
+ top: 0;
right: 4px;
- line-height: 40px;
+ line-height: 56px;
}
- .zen-leave-link {
+ a.js-zen-leave {
display: none;
color: $gl-text-color;
position: absolute;
@@ -25,62 +21,41 @@
}
}
- // Hide the Enter link when we're in Zen mode
- input:checked ~ .zen-backdrop .zen-enter-link {
- display: none;
- }
-
- // Show the Leave link when we're in Zen mode
- input:checked ~ .zen-backdrop .zen-leave-link {
- display: block;
- position: absolute;
- top: 0;
- }
-
- input:checked ~ .zen-backdrop {
- background-color: white;
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 1031;
-
- textarea {
- border: none;
- box-shadow: none;
- border-radius: 0;
- color: #000;
- font-size: 20px;
- line-height: 26px;
- padding: 30px;
- display: block;
- outline: none;
- resize: none;
- height: 100vh;
- max-width: 900px;
- margin: 0 auto;
+ .zen-backdrop {
+ &.fullscreen {
+ background-color: white;
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1031;
+
+ textarea {
+ border: none;
+ box-shadow: none;
+ border-radius: 0;
+ color: #000;
+ font-size: 20px;
+ line-height: 26px;
+ padding: 30px;
+ display: block;
+ outline: none;
+ resize: none;
+ height: 100vh;
+ max-width: 900px;
+ margin: 0 auto;
+ }
+
+ a.js-zen-enter {
+ display: none;
+ }
+
+ a.js-zen-leave {
+ display: block;
+ position: absolute;
+ top: 0;
+ }
}
}
-
- // Make the color of the placeholder text in the Zenned-out textarea darker,
- // so it becomes visible
-
- input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder {
- color: #A8A8A8;
- }
-
- input:checked ~ .zen-backdrop textarea:-moz-placeholder {
- color: #A8A8A8;
- opacity: 1;
- }
-
- input:checked ~ .zen-backdrop textarea::-moz-placeholder {
- color: #A8A8A8;
- opacity: 1;
- }
-
- input:checked ~ .zen-backdrop textarea:-ms-input-placeholder {
- color: #A8A8A8;
- }
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 6a2b25ddc67..47673944896 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,18 +1,38 @@
/* https://github.com/MozMorris/tomorrow-pygments */
.code.dark {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: #1d1f21;
+ }
+
+ .diff-line-num, .diff-line-num a {
+ color: rgba(255, 255, 255, 0.3);
+ }
- background-color: #1d1f21 !important;
- color: #c5c8c6 !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: #666;
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color: #1d1f21 !important;
- color: #c5c8c6 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #1d1f21;
+ color: #c5c8c6;
}
- pre.code {
- border-left: 1px solid #666;
+ // Diff line
+ .line_holder {
+ .diff-line-num.new, .line_content.new {
+ @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
+ }
+
+ .diff-line-num.old, .line_content.old {
+ @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080);
+ }
+
+ .line_content.match {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+ }
}
// highlight line via anchor
@@ -23,12 +43,12 @@
// Search result highlight
span.highlight_word {
background-color: #ffe792 !important;
- color: #000000 !important;
+ color: #000 !important;
}
.hll { background-color: #373b41 }
.c { color: #969896 } /* Comment */
- .err { color: #cc6666 } /* Error */
+ .err { color: #c66 } /* Error */
.k { color: #b294bb } /* Keyword */
.l { color: #de935f } /* Literal */
.n { color: #c5c8c6 } /* Name */
@@ -38,7 +58,7 @@
.cp { color: #969896 } /* Comment.Preproc */
.c1 { color: #969896 } /* Comment.Single */
.cs { color: #969896 } /* Comment.Special */
- .gd { color: #cc6666 } /* Generic.Deleted */
+ .gd { color: #c66 } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */
.gi { color: #b5bd68 } /* Generic.Inserted */
@@ -57,17 +77,17 @@
.na { color: #81a2be } /* Name.Attribute */
.nb { color: #c5c8c6 } /* Name.Builtin */
.nc { color: #f0c674 } /* Name.Class */
- .no { color: #cc6666 } /* Name.Constant */
+ .no { color: #c66 } /* Name.Constant */
.nd { color: #8abeb7 } /* Name.Decorator */
.ni { color: #c5c8c6 } /* Name.Entity */
- .ne { color: #cc6666 } /* Name.Exception */
+ .ne { color: #c66 } /* Name.Exception */
.nf { color: #81a2be } /* Name.Function */
.nl { color: #c5c8c6 } /* Name.Label */
.nn { color: #f0c674 } /* Name.Namespace */
.nx { color: #81a2be } /* Name.Other */
.py { color: #c5c8c6 } /* Name.Property */
.nt { color: #8abeb7 } /* Name.Tag */
- .nv { color: #cc6666 } /* Name.Variable */
+ .nv { color: #c66 } /* Name.Variable */
.ow { color: #8abeb7 } /* Operator.Word */
.w { color: #c5c8c6 } /* Text.Whitespace */
.mf { color: #de935f } /* Literal.Number.Float */
@@ -86,8 +106,8 @@
.s1 { color: #b5bd68 } /* Literal.String.Single */
.ss { color: #b5bd68 } /* Literal.String.Symbol */
.bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */
- .vc { color: #cc6666 } /* Name.Variable.Class */
- .vg { color: #cc6666 } /* Name.Variable.Global */
- .vi { color: #cc6666 } /* Name.Variable.Instance */
+ .vc { color: #c66 } /* Name.Variable.Class */
+ .vg { color: #c66 } /* Name.Variable.Global */
+ .vi { color: #c66 } /* Name.Variable.Instance */
.il { color: #de935f } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 8560c3c490f..806401c21ae 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,18 +1,38 @@
/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
.code.monokai {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: #272822;
+ }
+
+ .diff-line-num, .diff-line-num a {
+ color: rgba(255, 255, 255, 0.3);
+ }
- background-color: #272822 !important;
- color: #f8f8f2 !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: #555;
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color :#272822 !important;
- color: #f8f8f2 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #272822;
+ color: #f8f8f2;
}
- pre.code {
- border-left: 1px solid #555;
+ // Diff line
+ .line_holder {
+ .diff-line-num.new, .line_content.new {
+ @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
+ }
+
+ .diff-line-num.old, .line_content.old {
+ @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080);
+ }
+
+ .line_content.match {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+ }
}
// highlight line via anchor
@@ -23,7 +43,7 @@
// Search result highlight
span.highlight_word {
background-color: #ffe792 !important;
- color: #000000 !important;
+ color: #000 !important;
}
.hll { background-color: #49483e }
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 7d489a9666b..6a809d4dfd2 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,18 +1,38 @@
/* https://gist.github.com/qguv/7936275 */
.code.solarized-dark {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: #002b36;
+ }
+
+ .diff-line-num, .diff-line-num a {
+ color: rgba(255, 255, 255, 0.3);
+ }
- background-color: #002b36 !important;
- color: #93a1a1 !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: #113b46;
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color: #002b36 !important;
- color: #93a1a1 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #002b36;
+ color: #93a1a1;
}
- pre.code {
- border-left: 1px solid #113b46;
+ // Diff line
+ .line_holder {
+ .diff-line-num.new, .line_content.new {
+ @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
+ }
+
+ .diff-line-num.old, .line_content.old {
+ @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46);
+ }
+
+ .line_content.match {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+ }
}
// highlight line via anchor
@@ -76,7 +96,7 @@
.m { color: #2aa198 } /* Literal.Number */
.s { color: #2aa198 } /* Literal.String */
.na { color: #93a1a1 } /* Name.Attribute */
- .nb { color: #B58900 } /* Name.Builtin */
+ .nb { color: #b58900 } /* Name.Builtin */
.nc { color: #268bd2 } /* Name.Class */
.no { color: #cb4b16 } /* Name.Constant */
.nd { color: #268bd2 } /* Name.Decorator */
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 200ed346446..b90c95c62d1 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,18 +1,38 @@
/* https://gist.github.com/qguv/7936275 */
.code.solarized-light {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: #fdf6e3;
+ }
+
+ .diff-line-num, .diff-line-num a {
+ color: rgba(0, 0, 0, 0.3);
+ }
- background-color: #fdf6e3 !important;
- color: #586e75 !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: #c5d0d4;
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color: #fdf6e3 !important;
- color: #586e75 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #fdf6e3;
+ color: #586e75;
}
- pre.code {
- border-left: 1px solid #c5d0d4;
+ // Diff line
+ .line_holder {
+ .diff-line-num.new, .line_content.new {
+ @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
+ }
+
+ .diff-line-num.old, .line_content.old {
+ @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4);
+ }
+
+ .line_content.match {
+ color: rgba(0, 0, 0, 0.3);
+ background: rgba(255, 255, 255, 0.4);
+ }
}
// highlight line via anchor
@@ -76,7 +96,7 @@
.m { color: #2aa198 } /* Literal.Number */
.s { color: #2aa198 } /* Literal.String */
.na { color: #586e75 } /* Name.Attribute */
- .nb { color: #B58900 } /* Name.Builtin */
+ .nb { color: #b58900 } /* Name.Builtin */
.nc { color: #268bd2 } /* Name.Class */
.no { color: #cb4b16 } /* Name.Constant */
.nd { color: #268bd2 } /* Name.Decorator */
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index e2626da7871..8c1b0cd84ec 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,20 +1,60 @@
/* https://github.com/aahan/pygments-github-style */
.code.white {
+ // Line numbers
+ .line-numbers, .diff-line-num {
+ background-color: $background-color;
+ }
- background-color: #f8fafc !important;
- color: #5b6169 !important;
+ .diff-line-num, .diff-line-num a {
+ color: rgba(0, 0, 0, 0.3);
+ }
- pre.highlight,
- .line-numbers,
- .line-numbers a {
- background-color: $background-color !important;
- color: $gl-gray !important;
+ // Code itself
+ pre.code, .diff-line-num {
+ border-color: $border-color;
}
- pre.code {
- border-left: 1px solid $border-color;
- background-color: #fff !important;
- color: #333 !important;
+ &, pre.code, .line_holder .line_content {
+ background-color: #fff;
+ color: #333;
+ }
+
+ // Diff line
+ .line_holder {
+ .diff-line-num {
+ &.old {
+ background: #fdd;
+ border-color: #f1c0c0;
+ }
+
+ &.new {
+ background: #dbffdb;
+ border-color: #c1e9c1;
+ }
+ }
+
+ .line_content {
+ &.old {
+ background: #ffecec;
+
+ span.idiff {
+ background-color: #f8cbcb;
+ }
+ }
+
+ &.new {
+ background: #eaffea;
+
+ span.idiff {
+ background-color: #a6f3a6;
+ }
+ }
+
+ &.match {
+ color: rgba(0, 0, 0, 0.3);
+ background: #fafafa;
+ }
+ }
}
// highlight line via anchor
@@ -28,66 +68,66 @@
}
.hll { background-color: #f8f8f8 }
- .c { color: #999988; font-style: italic; }
+ .c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
.o { font-weight: bold; }
- .cm { color: #999988; font-style: italic; }
- .cp { color: #999999; font-weight: bold; }
- .c1 { color: #999988; font-style: italic; }
- .cs { color: #999999; font-weight: bold; font-style: italic; }
- .gd { color: #000000; background-color: #ffdddd; }
- .gd .x { color: #000000; background-color: #ffaaaa; }
+ .cm { color: #998; font-style: italic; }
+ .cp { color: #999; font-weight: bold; }
+ .c1 { color: #998; font-style: italic; }
+ .cs { color: #999; font-weight: bold; font-style: italic; }
+ .gd { color: #000; background-color: #fdd; }
+ .gd .x { color: #000; background-color: #faa; }
.ge { font-style: italic; }
- .gr { color: #aa0000; }
- .gh { color: #999999; }
- .gi { color: #000000; background-color: #ddffdd; }
- .gi .x { color: #000000; background-color: #aaffaa; }
- .go { color: #888888; }
- .gp { color: #555555; }
+ .gr { color: #a00; }
+ .gh { color: #999; }
+ .gi { color: #000; background-color: #dfd; }
+ .gi .x { color: #000; background-color: #afa; }
+ .go { color: #888; }
+ .gp { color: #555; }
.gs { font-weight: bold; }
.gu { color: #800080; font-weight: bold; }
- .gt { color: #aa0000; }
+ .gt { color: #a00; }
.kc { font-weight: bold; }
.kd { font-weight: bold; }
.kn { font-weight: bold; }
.kp { font-weight: bold; }
.kr { font-weight: bold; }
- .kt { color: #445588; font-weight: bold; }
- .m { color: #009999; }
- .s { color: #dd1144; }
- .n { color: #333333; }
+ .kt { color: #458; font-weight: bold; }
+ .m { color: #099; }
+ .s { color: #d14; }
+ .n { color: #333; }
.na { color: teal; }
.nb { color: #0086b3; }
- .nc { color: #445588; font-weight: bold; }
+ .nc { color: #458; font-weight: bold; }
.no { color: teal; }
.ni { color: purple; }
- .ne { color: #990000; font-weight: bold; }
- .nf { color: #990000; font-weight: bold; }
- .nn { color: #555555; }
+ .ne { color: #900; font-weight: bold; }
+ .nf { color: #900; font-weight: bold; }
+ .nn { color: #555; }
.nt { color: navy; }
.nv { color: teal; }
.ow { font-weight: bold; }
- .w { color: #bbbbbb; }
- .mf { color: #009999; }
- .mh { color: #009999; }
- .mi { color: #009999; }
- .mo { color: #009999; }
- .sb { color: #dd1144; }
- .sc { color: #dd1144; }
- .sd { color: #dd1144; }
- .s2 { color: #dd1144; }
- .se { color: #dd1144; }
- .sh { color: #dd1144; }
- .si { color: #dd1144; }
- .sx { color: #dd1144; }
+ .w { color: #bbb; }
+ .mf { color: #099; }
+ .mh { color: #099; }
+ .mi { color: #099; }
+ .mo { color: #099; }
+ .sb { color: #d14; }
+ .sc { color: #d14; }
+ .sd { color: #d14; }
+ .s2 { color: #d14; }
+ .se { color: #d14; }
+ .sh { color: #d14; }
+ .si { color: #d14; }
+ .sx { color: #d14; }
.sr { color: #009926; }
- .s1 { color: #dd1144; }
+ .s1 { color: #d14; }
.ss { color: #990073; }
- .bp { color: #999999; }
+ .bp { color: #999; }
.vc { color: teal; }
.vg { color: teal; }
.vi { color: teal; }
- .il { color: #009999; }
- .gc { color: #999; background-color: #EAF2F5; }
+ .il { color: #099; }
+ .gc { color: #999; background-color: #eaf2f5; }
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 144852e7874..a61161810a3 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -55,6 +55,16 @@
@extend .alert-warning;
padding: 10px;
text-align: center;
+
+ > div, p {
+ display: inline;
+ margin: 0;
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+ }
+ }
}
.broadcast-message-preview {
diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/pages/appearances.scss
new file mode 100644
index 00000000000..878f44116ba
--- /dev/null
+++ b/app/assets/stylesheets/pages/appearances.scss
@@ -0,0 +1,11 @@
+.appearance-logo-preview {
+ max-width: 400px;
+ margin-bottom: 20px;
+}
+
+.appearance-light-logo-preview {
+ background-color: $background-color;
+ max-width: 72px;
+ padding: 10px;
+ margin-bottom: 10px;
+}
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 87dd30f4111..28994e60baa 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,125 +1,133 @@
.awards {
- @include clearfix;
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
- margin: 7px 0 0 5px;
}
+}
- .award {
- @include border-radius(5px);
-
- border: 1px solid;
- padding: 0px 10px;
- float: left;
- margin-right: 5px;
- border-color: $border-color;
- cursor: pointer;
+.emoji-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ margin-top: 3px;
+ z-index: 1000;
+ min-width: 160px;
+ font-size: 14px;
+ background-color: $award-emoji-menu-bg;
+ border: 1px solid $award-emoji-menu-border;
+ border-radius: $border-radius-base;
+ box-shadow: 0 6px 12px rgba(0,0,0,.175);
+ pointer-events: none;
+ opacity: 0;
+ transform: scale(.2);
+ transform-origin: 0 -45px;
+ transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
+
+ &.is-visible {
+ pointer-events: all;
+ opacity: 1;
+ transform: scale(1);
+ }
- &:hover {
- background-color: #dce0e5;
+ .emoji-menu-content {
+ padding: $gl-padding;
+ width: 300px;
+ height: 300px;
+ overflow-y: scroll;
+
+ input.emoji-search{
+ 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");
+ background-repeat: no-repeat;
+ background-position: right 5px center;
+ background-size: 16px;
}
+ }
+}
- &.active {
- border-color: $border-gray-light;
- background-color: $gray-light;
-
- &:hover {
- background-color: #dce0e5;
- }
+.emoji-menu-list {
+ list-style: none;
+ padding-left: 0;
+ margin-bottom: 0;
+}
- .counter {
- font-weight: bold;
- }
- }
+.emoji-menu-list-item {
+ padding: 3px;
+ margin-left: 1px;
+ margin-right: 1px;
+}
- .icon {
- float: left;
- margin-right: 10px;
- }
+.emoji-menu-btn {
+ display: block;
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: $border-radius-base;
+ transition: transform .15s cubic-bezier(.3, 0, .2, 2);
+
+ &:hover {
+ background-color: transparent;
+ outline: 0;
+ transform: scale(1.3);
+ }
- .counter {
- float: left;
- }
+ &:focus,
+ &:active {
+ outline: 0;
}
- .awards-controls {
+ .emoji-icon {
+ display: inline-block;
position: relative;
- margin-left: 10px;
- float: left;
+ top: 3px;
+ }
+}
- .add-award {
- font-size: 24px;
- color: $gl-gray;
- position: relative;
- top: 2px;
+.award-menu-holder {
+ display: inline-block;
+ position: relative;
+}
- &:hover,
- &:link {
- text-decoration: none;
- }
- }
+.award-control {
+ margin-right: 5px;
+ padding-left: 5px;
+ padding-right: 5px;
+ line-height: 20px;
+ outline: 0;
+
+ &.active,
+ &:active {
+ background-color: $white-dark;
+ box-shadow: none;
+ outline: 0;
+ }
- .emoji-menu{
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
+ &.is-loading {
+ .award-control-icon {
display: none;
- float: left;
- min-width: 160px;
- padding: 5px 0;
- margin: 2px 0 0;
- font-size: 14px;
- text-align: left;
- list-style: none;
- background-color: #fff;
- -webkit-background-clip: padding-box;
- background-clip: padding-box;
- border: 1px solid #ccc;
- border: 1px solid rgba(0,0,0,.15);
- border-radius: 4px;
- -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
- box-shadow: 0 6px 12px rgba(0,0,0,.175);
-
- .emoji-menu-content {
- padding: $gl-padding;
- width: 300px;
- height: 300px;
- overflow-y: scroll;
-
- h5 {
- clear: left;
- }
-
- ul {
- list-style-type: none;
- margin-left: -20px;
- margin-bottom: 20px;
- overflow: auto;
- }
-
- input.emoji-search{
- background: image-url("icon-search.png") 240px no-repeat;
- }
-
- li {
- cursor: pointer;
- width: 30px;
- height: 30px;
- text-align: center;
- float: left;
- margin: 3px;
- list-decorate: none;
- @include border-radius(5px);
-
- &:hover {
- background-color: #ccc;
- }
- }
- }
}
+
+ .award-control-icon-loading {
+ display: block;
+ }
+ }
+
+ .icon,
+ .award-control-icon {
+ float: left;
+ margin-right: 5px;
+ font-size: 20px;
+ }
+
+ .award-control-icon-loading {
+ display: none;
+ }
+
+ .award-control-icon {
+ color: $award-emoji-new-btn-icon-color;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 3c2997c1d5a..201f3e5ca46 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,6 +1,6 @@
.build-page {
pre.trace {
- background: #111111;
+ background: #111;
color: #fff;
font-family: $monospace_font;
white-space: pre;
@@ -27,10 +27,25 @@
}
.scroll-controls {
- position: fixed;
- bottom: 10px;
- left: 250px;
- z-index: 100;
+ &.affix-top {
+ position: absolute;
+ top: 10px;
+ right: 25px;
+ }
+
+ &.affix-bottom {
+ position: absolute;
+ right: 25px;
+ }
+
+ &.affix {
+ right: 30px;
+ bottom: 15px;
+
+ @media (min-width: $screen-md-min) {
+ right: 26%;
+ }
+ }
a {
display: block;
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 17245d3be7b..971656feb42 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -35,6 +35,8 @@
}
.commit-box {
+ border-top: 1px solid $border-color;
+
.commit-title {
margin: 0;
font-size: 23px;
@@ -53,7 +55,7 @@
padding: 10px 0;
li {
- padding: 3px 0px;
+ padding: 3px 0;
line-height: 20px;
}
}
@@ -88,6 +90,7 @@
position: relative;
font-family: $monospace_font;
$left: 12px;
+ overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker {
width: 72ch;
color: rgba(0, 0, 0, 0.0);
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c9dfcff6290..d57be1b2daa 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -9,7 +9,7 @@
.lists-separator {
margin: 10px 0;
- border-color: #DDD;
+ border-color: #ddd;
}
.commits-row {
@@ -28,10 +28,6 @@
}
}
-.commits-feed-holder {
- float: right;
-}
-
li.commit {
list-style: none;
@@ -40,6 +36,10 @@ li.commit {
line-height: 20px;
margin-bottom: 2px;
+ .btn-clipboard {
+ margin-top: -1px;
+ }
+
.notes_count {
float: right;
margin-right: 10px;
@@ -76,7 +76,7 @@ li.commit {
.commit-row-description {
font-size: 14px;
- border-left: 1px solid #EEE;
+ border-left: 1px solid #eee;
padding: 10px 15px;
margin: 5px 0 10px 5px;
background: #f9f9f9;
@@ -122,3 +122,59 @@ li.commit {
color: $gl-gray;
}
}
+
+.divergence-graph {
+ padding: 12px 12px 0 0;
+ float: right;
+
+ .graph-side {
+ position: relative;
+ width: 80px;
+ height: 22px;
+ padding: 5px 0 13px;
+ float: left;
+
+ .bar {
+ position: absolute;
+ height: 4px;
+ background-color: #ccc;
+ }
+
+ .bar-behind {
+ right: 0;
+ border-radius: 3px 0 0 3px;
+ }
+
+ .bar-ahead {
+ left: 0;
+ border-radius: 0 3px 3px 0;
+ }
+
+ .count {
+ padding-top: 6px;
+ padding-bottom: 0;
+ font-size: 12px;
+ color: #333;
+ display: block;
+ }
+
+ .count-behind {
+ padding-right: 4px;
+ text-align: right;
+ }
+
+ .count-ahead {
+ padding-left: 4px;
+ text-align: left;
+ }
+ }
+
+ .graph-separator {
+ position: relative;
+ width: 1px;
+ height: 18px;
+ margin: 5px 0 0;
+ float: left;
+ background-color: #ccc;
+ }
+}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 25a86cd0f94..cf7567513ec 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -11,15 +11,15 @@
}
.dashboard-search-filter {
- padding:5px;
+ padding: 5px;
.search-text-input {
- float:left;
+ float: left;
@extend .col-md-2;
}
.btn {
margin-left: 5px;
- float:left;
+ float: left;
}
}
@@ -40,10 +40,6 @@
.avatar {
@include border-radius(50%);
}
-
- .identicon {
- line-height: 46px;
- }
}
.dash-project-access-icon {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index deab805dbc2..d3eda1a57e6 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -1,7 +1,5 @@
.detail-page-header {
- margin: -$gl-padding;
- padding: 7px $gl-padding;
- margin-bottom: 0px;
+ padding: 11px 0;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
@@ -14,6 +12,15 @@
.identifier {
color: #5c5d5e;
}
+
+ .issue_created_ago, .author_link {
+ white-space: nowrap;
+ }
+
+ .issue-meta {
+ display: inline-block;
+ line-height: 20px;
+ }
}
.detail-page-description {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index afd6fb73675..d5862a11aca 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,9 +1,7 @@
// Common
.diff-file {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- border: none;
- border-bottom: 1px solid #E7E9EE;
+ border: 1px solid $border-color;
+ margin-bottom: $gl-padding;
.diff-header {
position: relative;
@@ -23,14 +21,6 @@
}
}
- .diff-controls {
- .btn {
- padding: 0px 10px;
- font-size: 13px;
- line-height: 28px;
- }
- }
-
.commit-short-id {
font-family: $monospace_font;
font-size: smaller;
@@ -39,19 +29,9 @@
.diff-content {
overflow: auto;
overflow-y: hidden;
- background: #FFF;
+ background: #fff;
color: #333;
- .old {
- span.idiff {
- background-color: #f8cbcb;
- }
- }
- .new {
- span.idiff {
- background-color: #a6f3a6;
- }
- }
.unfold {
cursor: pointer;
}
@@ -76,8 +56,9 @@
width: 100%;
font-family: $monospace_font;
border: none;
- margin: 0px;
- padding: 0px;
+ border-collapse: separate;
+ margin: 0;
+ padding: 0;
.line_holder td {
line-height: $code_line_height;
font-size: $code_font_size;
@@ -85,7 +66,7 @@
}
tr.line_holder.parallel {
- .old_line, .new_line, .diff_line {
+ .old_line, .new_line {
min-width: 50px;
}
@@ -94,14 +75,12 @@
}
}
- .old_line, .new_line, .diff_line {
- margin: 0px;
- padding: 0px;
+ .old_line, .new_line {
+ margin: 0;
+ padding: 0;
border: none;
- background: $background-color;
- color: rgba(0, 0, 0, 0.3);
- padding: 0px 5px;
- border-right: 1px solid $border-color;
+ padding: 0 5px;
+ border-right: 1px solid;
text-align: right;
min-width: 35px;
max-width: 50px;
@@ -111,48 +90,16 @@
float: left;
width: 35px;
font-weight: normal;
- color: rgba(0, 0, 0, 0.3);
&:hover {
text-decoration: underline;
}
}
- &.new {
- background: #CFD;
- }
- &.old {
- background: #FDD;
- }
- }
- .diff_line {
- padding: 0;
- }
- .line_holder {
- &.old .old_line,
- &.old .new_line {
- background: #ffdddd;
- border-color: #f1c0c0;
- }
- &.new .old_line,
- &.new .new_line {
- background: #dbffdb;
- border-color: #c1e9c1;
- }
}
.line_content {
display: block;
- margin: 0px;
- padding: 0px 0.5em;
+ margin: 0;
+ padding: 0 0.5em;
border: none;
- &.new {
- background: #eaffea;
- }
- &.old {
- background: #ffecec;
- }
- &.matched {
- color: $border-color;
- background: #fafafa;
- }
&.parallel {
display: table-cell;
}
@@ -171,7 +118,7 @@
background-color: #fff;
line-height: 0;
img {
- border: 1px solid #FFF;
+ border: 1px solid #fff;
background: image-url('trans_bg.gif');
max-width: 100%;
}
@@ -236,7 +183,7 @@
height: 14px;
width: 15px;
position: absolute;
- top: 0px;
+ top: 0;
background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
}
.bottom-handle {
@@ -244,7 +191,7 @@
height: 14px;
width: 15px;
position: absolute;
- bottom: 0px;
+ bottom: 0;
background: image-url('swipemode_sprites.gif') 0 -11px no-repeat;
}
}
@@ -259,8 +206,8 @@
.frame.added, .frame.deleted {
position: absolute;
display: block;
- top: 0px;
- left: 0px;
+ top: 0;
+ left: 0;
}
.controls {
display: block;
@@ -268,7 +215,7 @@
width: 300px;
z-index: 100;
position: absolute;
- bottom: 0px;
+ bottom: 0;
left: 50%;
margin-left: -150px;
@@ -284,11 +231,11 @@
.dragger {
display: block;
position: absolute;
- left: 0px;
- top: 0px;
+ left: 0;
+ top: 0;
height: 14px;
width: 14px;
- background: image-url('onion_skin_sprites.gif') 0px -34px repeat-x;
+ background: image-url('onion_skin_sprites.gif') 0 -34px repeat-x;
cursor: pointer;
}
@@ -296,17 +243,17 @@
display: block;
position: absolute;
top: 2px;
- right: 0px;
+ right: 0;
height: 10px;
width: 10px;
- background: image-url('onion_skin_sprites.gif') -2px 0px no-repeat;
+ background: image-url('onion_skin_sprites.gif') -2px 0 no-repeat;
}
.opaque {
display: block;
position: absolute;
top: 2px;
- left: 0px;
+ left: 0;
height: 10px;
width: 10px;
background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat;
@@ -318,7 +265,7 @@
.view-modes {
padding: 10px;
text-align: center;
- background: #EEE;
+ background: #eee;
ul, li {
list-style: none;
@@ -402,3 +349,23 @@
right: 15px;
}
}
+
+@mixin diff_background($background, $idiff, $border) {
+ background: $background;
+
+ &.line_content span.idiff {
+ background: $idiff;
+ }
+
+ &.diff-line-num {
+ border-color: $border;
+ }
+}
+
+.files {
+ margin-top: -1px;
+
+ .diff-file:last-child {
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 39d916cd336..43be5e38ba8 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -14,9 +14,9 @@
}
.cancel-btn {
- color: #B94A48;
+ color: #b94a48;
&:hover {
- color: #B94A48;
+ color: #b94a48;
}
}
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss
index 89a94c5a780..b731abc7450 100644
--- a/app/assets/stylesheets/pages/emojis.scss
+++ b/app/assets/stylesheets/pages/emojis.scss
@@ -1,1272 +1,1736 @@
-/*
-File is generated by https://github.com/jakesgordon/sprite-factory and midified manualy
-The source: gemojione gem.
-*/
+.emoji-0023-20E3 { background-position: 0 0; }
+.emoji-002A-20E3 { background-position: -20px 0; }
+.emoji-0030-20E3 { background-position: 0 -20px; }
+.emoji-0031-20E3 { background-position: -20px -20px; }
+.emoji-0032-20E3 { background-position: -40px 0; }
+.emoji-0033-20E3 { background-position: -40px -20px; }
+.emoji-0034-20E3 { background-position: 0 -40px; }
+.emoji-0035-20E3 { background-position: -20px -40px; }
+.emoji-0036-20E3 { background-position: -40px -40px; }
+.emoji-0037-20E3 { background-position: -60px 0; }
+.emoji-0038-20E3 { background-position: -60px -20px; }
+.emoji-0039-20E3 { background-position: -60px -40px; }
+.emoji-00A9 { background-position: 0 -60px; }
+.emoji-00AE { background-position: -20px -60px; }
+.emoji-1F004 { background-position: -40px -60px; }
+.emoji-1F0CF { background-position: -60px -60px; }
+.emoji-1F170 { background-position: -80px 0; }
+.emoji-1F171 { background-position: -80px -20px; }
+.emoji-1F17E { background-position: -80px -40px; }
+.emoji-1F17F { background-position: -80px -60px; }
+.emoji-1F18E { background-position: 0 -80px; }
+.emoji-1F191 { background-position: -20px -80px; }
+.emoji-1F192 { background-position: -40px -80px; }
+.emoji-1F193 { background-position: -60px -80px; }
+.emoji-1F194 { background-position: -80px -80px; }
+.emoji-1F195 { background-position: -100px 0; }
+.emoji-1F196 { background-position: -100px -20px; }
+.emoji-1F197 { background-position: -100px -40px; }
+.emoji-1F198 { background-position: -100px -60px; }
+.emoji-1F199 { background-position: -100px -80px; }
+.emoji-1F19A { background-position: 0 -100px; }
+.emoji-1F1E6-1F1E8 { background-position: -20px -100px; }
+.emoji-1F1E6-1F1E9 { background-position: -40px -100px; }
+.emoji-1F1E6-1F1EA { background-position: -60px -100px; }
+.emoji-1F1E6-1F1EB { background-position: -80px -100px; }
+.emoji-1F1E6-1F1EC { background-position: -100px -100px; }
+.emoji-1F1E6-1F1EE { background-position: -120px 0; }
+.emoji-1F1E6-1F1F1 { background-position: -120px -20px; }
+.emoji-1F1E6-1F1F2 { background-position: -120px -40px; }
+.emoji-1F1E6-1F1F4 { background-position: -120px -60px; }
+.emoji-1F1E6-1F1F6 { background-position: -120px -80px; }
+.emoji-1F1E6-1F1F7 { background-position: -120px -100px; }
+.emoji-1F1E6-1F1F8 { background-position: 0 -120px; }
+.emoji-1F1E6-1F1F9 { background-position: -20px -120px; }
+.emoji-1F1E6-1F1FA { background-position: -40px -120px; }
+.emoji-1F1E6-1F1FC { background-position: -60px -120px; }
+.emoji-1F1E6-1F1FD { background-position: -80px -120px; }
+.emoji-1F1E6-1F1FF { background-position: -100px -120px; }
+.emoji-1F1E7-1F1E6 { background-position: -120px -120px; }
+.emoji-1F1E7-1F1E7 { background-position: -140px 0; }
+.emoji-1F1E7-1F1E9 { background-position: -140px -20px; }
+.emoji-1F1E7-1F1EA { background-position: -140px -40px; }
+.emoji-1F1E7-1F1EB { background-position: -140px -60px; }
+.emoji-1F1E7-1F1EC { background-position: -140px -80px; }
+.emoji-1F1E7-1F1ED { background-position: -140px -100px; }
+.emoji-1F1E7-1F1EE { background-position: -140px -120px; }
+.emoji-1F1E7-1F1EF { background-position: 0 -140px; }
+.emoji-1F1E7-1F1F1 { background-position: -20px -140px; }
+.emoji-1F1E7-1F1F2 { background-position: -40px -140px; }
+.emoji-1F1E7-1F1F3 { background-position: -60px -140px; }
+.emoji-1F1E7-1F1F4 { background-position: -80px -140px; }
+.emoji-1F1E7-1F1F6 { background-position: -100px -140px; }
+.emoji-1F1E7-1F1F7 { background-position: -120px -140px; }
+.emoji-1F1E7-1F1F8 { background-position: -140px -140px; }
+.emoji-1F1E7-1F1F9 { background-position: -160px 0; }
+.emoji-1F1E7-1F1FB { background-position: -160px -20px; }
+.emoji-1F1E7-1F1FC { background-position: -160px -40px; }
+.emoji-1F1E7-1F1FE { background-position: -160px -60px; }
+.emoji-1F1E7-1F1FF { background-position: -160px -80px; }
+.emoji-1F1E8-1F1E6 { background-position: -160px -100px; }
+.emoji-1F1E8-1F1E8 { background-position: -160px -120px; }
+.emoji-1F1E8-1F1E9 { background-position: -160px -140px; }
+.emoji-1F1E8-1F1EB { background-position: 0 -160px; }
+.emoji-1F1E8-1F1EC { background-position: -20px -160px; }
+.emoji-1F1E8-1F1ED { background-position: -40px -160px; }
+.emoji-1F1E8-1F1EE { background-position: -60px -160px; }
+.emoji-1F1E8-1F1F0 { background-position: -80px -160px; }
+.emoji-1F1E8-1F1F1 { background-position: -100px -160px; }
+.emoji-1F1E8-1F1F2 { background-position: -120px -160px; }
+.emoji-1F1E8-1F1F3 { background-position: -140px -160px; }
+.emoji-1F1E8-1F1F4 { background-position: -160px -160px; }
+.emoji-1F1E8-1F1F5 { background-position: -180px 0; }
+.emoji-1F1E8-1F1F7 { background-position: -180px -20px; }
+.emoji-1F1E8-1F1FA { background-position: -180px -40px; }
+.emoji-1F1E8-1F1FB { background-position: -180px -60px; }
+.emoji-1F1E8-1F1FC { background-position: -180px -80px; }
+.emoji-1F1E8-1F1FD { background-position: -180px -100px; }
+.emoji-1F1E8-1F1FE { background-position: -180px -120px; }
+.emoji-1F1E8-1F1FF { background-position: -180px -140px; }
+.emoji-1F1E9-1F1EA { background-position: -180px -160px; }
+.emoji-1F1E9-1F1EC { background-position: 0 -180px; }
+.emoji-1F1E9-1F1EF { background-position: -20px -180px; }
+.emoji-1F1E9-1F1F0 { background-position: -40px -180px; }
+.emoji-1F1E9-1F1F2 { background-position: -60px -180px; }
+.emoji-1F1E9-1F1F4 { background-position: -80px -180px; }
+.emoji-1F1E9-1F1FF { background-position: -100px -180px; }
+.emoji-1F1EA-1F1E6 { background-position: -120px -180px; }
+.emoji-1F1EA-1F1E8 { background-position: -140px -180px; }
+.emoji-1F1EA-1F1EA { background-position: -160px -180px; }
+.emoji-1F1EA-1F1EC { background-position: -180px -180px; }
+.emoji-1F1EA-1F1ED { background-position: -200px 0; }
+.emoji-1F1EA-1F1F7 { background-position: -200px -20px; }
+.emoji-1F1EA-1F1F8 { background-position: -200px -40px; }
+.emoji-1F1EA-1F1F9 { background-position: -200px -60px; }
+.emoji-1F1EA-1F1FA { background-position: -200px -80px; }
+.emoji-1F1EB-1F1EE { background-position: -200px -100px; }
+.emoji-1F1EB-1F1EF { background-position: -200px -120px; }
+.emoji-1F1EB-1F1F0 { background-position: -200px -140px; }
+.emoji-1F1EB-1F1F2 { background-position: -200px -160px; }
+.emoji-1F1EB-1F1F4 { background-position: -200px -180px; }
+.emoji-1F1EB-1F1F7 { background-position: 0 -200px; }
+.emoji-1F1EC-1F1E6 { background-position: -20px -200px; }
+.emoji-1F1EC-1F1E7 { background-position: -40px -200px; }
+.emoji-1F1EC-1F1E9 { background-position: -60px -200px; }
+.emoji-1F1EC-1F1EA { background-position: -80px -200px; }
+.emoji-1F1EC-1F1EB { background-position: -100px -200px; }
+.emoji-1F1EC-1F1EC { background-position: -120px -200px; }
+.emoji-1F1EC-1F1ED { background-position: -140px -200px; }
+.emoji-1F1EC-1F1EE { background-position: -160px -200px; }
+.emoji-1F1EC-1F1F1 { background-position: -180px -200px; }
+.emoji-1F1EC-1F1F2 { background-position: -200px -200px; }
+.emoji-1F1EC-1F1F3 { background-position: -220px 0; }
+.emoji-1F1EC-1F1F5 { background-position: -220px -20px; }
+.emoji-1F1EC-1F1F6 { background-position: -220px -40px; }
+.emoji-1F1EC-1F1F7 { background-position: -220px -60px; }
+.emoji-1F1EC-1F1F8 { background-position: -220px -80px; }
+.emoji-1F1EC-1F1F9 { background-position: -220px -100px; }
+.emoji-1F1EC-1F1FA { background-position: -220px -120px; }
+.emoji-1F1EC-1F1FC { background-position: -220px -140px; }
+.emoji-1F1EC-1F1FE { background-position: -220px -160px; }
+.emoji-1F1ED-1F1F0 { background-position: -220px -180px; }
+.emoji-1F1ED-1F1F2 { background-position: -220px -200px; }
+.emoji-1F1ED-1F1F3 { background-position: 0 -220px; }
+.emoji-1F1ED-1F1F7 { background-position: -20px -220px; }
+.emoji-1F1ED-1F1F9 { background-position: -40px -220px; }
+.emoji-1F1ED-1F1FA { background-position: -60px -220px; }
+.emoji-1F1EE-1F1E8 { background-position: -80px -220px; }
+.emoji-1F1EE-1F1E9 { background-position: -100px -220px; }
+.emoji-1F1EE-1F1EA { background-position: -120px -220px; }
+.emoji-1F1EE-1F1F1 { background-position: -140px -220px; }
+.emoji-1F1EE-1F1F2 { background-position: -160px -220px; }
+.emoji-1F1EE-1F1F3 { background-position: -180px -220px; }
+.emoji-1F1EE-1F1F4 { background-position: -200px -220px; }
+.emoji-1F1EE-1F1F6 { background-position: -220px -220px; }
+.emoji-1F1EE-1F1F7 { background-position: -240px 0; }
+.emoji-1F1EE-1F1F8 { background-position: -240px -20px; }
+.emoji-1F1EE-1F1F9 { background-position: -240px -40px; }
+.emoji-1F1EF-1F1EA { background-position: -240px -60px; }
+.emoji-1F1EF-1F1F2 { background-position: -240px -80px; }
+.emoji-1F1EF-1F1F4 { background-position: -240px -100px; }
+.emoji-1F1EF-1F1F5 { background-position: -240px -120px; }
+.emoji-1F1F0-1F1EA { background-position: -240px -140px; }
+.emoji-1F1F0-1F1EC { background-position: -240px -160px; }
+.emoji-1F1F0-1F1ED { background-position: -240px -180px; }
+.emoji-1F1F0-1F1EE { background-position: -240px -200px; }
+.emoji-1F1F0-1F1F2 { background-position: -240px -220px; }
+.emoji-1F1F0-1F1F3 { background-position: 0 -240px; }
+.emoji-1F1F0-1F1F5 { background-position: -20px -240px; }
+.emoji-1F1F0-1F1F7 { background-position: -40px -240px; }
+.emoji-1F1F0-1F1FC { background-position: -60px -240px; }
+.emoji-1F1F0-1F1FE { background-position: -80px -240px; }
+.emoji-1F1F0-1F1FF { background-position: -100px -240px; }
+.emoji-1F1F1-1F1E6 { background-position: -120px -240px; }
+.emoji-1F1F1-1F1E7 { background-position: -140px -240px; }
+.emoji-1F1F1-1F1E8 { background-position: -160px -240px; }
+.emoji-1F1F1-1F1EE { background-position: -180px -240px; }
+.emoji-1F1F1-1F1F0 { background-position: -200px -240px; }
+.emoji-1F1F1-1F1F7 { background-position: -220px -240px; }
+.emoji-1F1F1-1F1F8 { background-position: -240px -240px; }
+.emoji-1F1F1-1F1F9 { background-position: -260px 0; }
+.emoji-1F1F1-1F1FA { background-position: -260px -20px; }
+.emoji-1F1F1-1F1FB { background-position: -260px -40px; }
+.emoji-1F1F1-1F1FE { background-position: -260px -60px; }
+.emoji-1F1F2-1F1E6 { background-position: -260px -80px; }
+.emoji-1F1F2-1F1E8 { background-position: -260px -100px; }
+.emoji-1F1F2-1F1E9 { background-position: -260px -120px; }
+.emoji-1F1F2-1F1EA { background-position: -260px -140px; }
+.emoji-1F1F2-1F1EB { background-position: -260px -160px; }
+.emoji-1F1F2-1F1EC { background-position: -260px -180px; }
+.emoji-1F1F2-1F1ED { background-position: -260px -200px; }
+.emoji-1F1F2-1F1F0 { background-position: -260px -220px; }
+.emoji-1F1F2-1F1F1 { background-position: -260px -240px; }
+.emoji-1F1F2-1F1F2 { background-position: 0 -260px; }
+.emoji-1F1F2-1F1F3 { background-position: -20px -260px; }
+.emoji-1F1F2-1F1F4 { background-position: -40px -260px; }
+.emoji-1F1F2-1F1F5 { background-position: -60px -260px; }
+.emoji-1F1F2-1F1F6 { background-position: -80px -260px; }
+.emoji-1F1F2-1F1F7 { background-position: -100px -260px; }
+.emoji-1F1F2-1F1F8 { background-position: -120px -260px; }
+.emoji-1F1F2-1F1F9 { background-position: -140px -260px; }
+.emoji-1F1F2-1F1FA { background-position: -160px -260px; }
+.emoji-1F1F2-1F1FB { background-position: -180px -260px; }
+.emoji-1F1F2-1F1FC { background-position: -200px -260px; }
+.emoji-1F1F2-1F1FD { background-position: -220px -260px; }
+.emoji-1F1F2-1F1FE { background-position: -240px -260px; }
+.emoji-1F1F2-1F1FF { background-position: -260px -260px; }
+.emoji-1F1F3-1F1E6 { background-position: -280px 0; }
+.emoji-1F1F3-1F1E8 { background-position: -280px -20px; }
+.emoji-1F1F3-1F1EA { background-position: -280px -40px; }
+.emoji-1F1F3-1F1EB { background-position: -280px -60px; }
+.emoji-1F1F3-1F1EC { background-position: -280px -80px; }
+.emoji-1F1F3-1F1EE { background-position: -280px -100px; }
+.emoji-1F1F3-1F1F1 { background-position: -280px -120px; }
+.emoji-1F1F3-1F1F4 { background-position: -280px -140px; }
+.emoji-1F1F3-1F1F5 { background-position: -280px -160px; }
+.emoji-1F1F3-1F1F7 { background-position: -280px -180px; }
+.emoji-1F1F3-1F1FA { background-position: -280px -200px; }
+.emoji-1F1F3-1F1FF { background-position: -280px -220px; }
+.emoji-1F1F4-1F1F2 { background-position: -280px -240px; }
+.emoji-1F1F5-1F1E6 { background-position: -280px -260px; }
+.emoji-1F1F5-1F1EA { background-position: 0 -280px; }
+.emoji-1F1F5-1F1EB { background-position: -20px -280px; }
+.emoji-1F1F5-1F1EC { background-position: -40px -280px; }
+.emoji-1F1F5-1F1ED { background-position: -60px -280px; }
+.emoji-1F1F5-1F1F0 { background-position: -80px -280px; }
+.emoji-1F1F5-1F1F1 { background-position: -100px -280px; }
+.emoji-1F1F5-1F1F2 { background-position: -120px -280px; }
+.emoji-1F1F5-1F1F3 { background-position: -140px -280px; }
+.emoji-1F1F5-1F1F7 { background-position: -160px -280px; }
+.emoji-1F1F5-1F1F8 { background-position: -180px -280px; }
+.emoji-1F1F5-1F1F9 { background-position: -200px -280px; }
+.emoji-1F1F5-1F1FC { background-position: -220px -280px; }
+.emoji-1F1F5-1F1FE { background-position: -240px -280px; }
+.emoji-1F1F6-1F1E6 { background-position: -260px -280px; }
+.emoji-1F1F7-1F1EA { background-position: -280px -280px; }
+.emoji-1F1F7-1F1F4 { background-position: -300px 0; }
+.emoji-1F1F7-1F1F8 { background-position: -300px -20px; }
+.emoji-1F1F7-1F1FA { background-position: -300px -40px; }
+.emoji-1F1F7-1F1FC { background-position: -300px -60px; }
+.emoji-1F1F8-1F1E6 { background-position: -300px -80px; }
+.emoji-1F1F8-1F1E7 { background-position: -300px -100px; }
+.emoji-1F1F8-1F1E8 { background-position: -300px -120px; }
+.emoji-1F1F8-1F1E9 { background-position: -300px -140px; }
+.emoji-1F1F8-1F1EA { background-position: -300px -160px; }
+.emoji-1F1F8-1F1EC { background-position: -300px -180px; }
+.emoji-1F1F8-1F1ED { background-position: -300px -200px; }
+.emoji-1F1F8-1F1EE { background-position: -300px -220px; }
+.emoji-1F1F8-1F1EF { background-position: -300px -240px; }
+.emoji-1F1F8-1F1F0 { background-position: -300px -260px; }
+.emoji-1F1F8-1F1F1 { background-position: -300px -280px; }
+.emoji-1F1F8-1F1F2 { background-position: 0 -300px; }
+.emoji-1F1F8-1F1F3 { background-position: -20px -300px; }
+.emoji-1F1F8-1F1F4 { background-position: -40px -300px; }
+.emoji-1F1F8-1F1F7 { background-position: -60px -300px; }
+.emoji-1F1F8-1F1F8 { background-position: -80px -300px; }
+.emoji-1F1F8-1F1F9 { background-position: -100px -300px; }
+.emoji-1F1F8-1F1FB { background-position: -120px -300px; }
+.emoji-1F1F8-1F1FD { background-position: -140px -300px; }
+.emoji-1F1F8-1F1FE { background-position: -160px -300px; }
+.emoji-1F1F8-1F1FF { background-position: -180px -300px; }
+.emoji-1F1F9-1F1E6 { background-position: -200px -300px; }
+.emoji-1F1F9-1F1E8 { background-position: -220px -300px; }
+.emoji-1F1F9-1F1E9 { background-position: -240px -300px; }
+.emoji-1F1F9-1F1EB { background-position: -260px -300px; }
+.emoji-1F1F9-1F1EC { background-position: -280px -300px; }
+.emoji-1F1F9-1F1ED { background-position: -300px -300px; }
+.emoji-1F1F9-1F1EF { background-position: -320px 0; }
+.emoji-1F1F9-1F1F0 { background-position: -320px -20px; }
+.emoji-1F1F9-1F1F1 { background-position: -320px -40px; }
+.emoji-1F1F9-1F1F2 { background-position: -320px -60px; }
+.emoji-1F1F9-1F1F3 { background-position: -320px -80px; }
+.emoji-1F1F9-1F1F4 { background-position: -320px -100px; }
+.emoji-1F1F9-1F1F7 { background-position: -320px -120px; }
+.emoji-1F1F9-1F1F9 { background-position: -320px -140px; }
+.emoji-1F1F9-1F1FB { background-position: -320px -160px; }
+.emoji-1F1F9-1F1FC { background-position: -320px -180px; }
+.emoji-1F1F9-1F1FF { background-position: -320px -200px; }
+.emoji-1F1FA-1F1E6 { background-position: -320px -220px; }
+.emoji-1F1FA-1F1EC { background-position: -320px -240px; }
+.emoji-1F1FA-1F1F2 { background-position: -320px -260px; }
+.emoji-1F1FA-1F1F8 { background-position: -320px -280px; }
+.emoji-1F1FA-1F1FE { background-position: -320px -300px; }
+.emoji-1F1FA-1F1FF { background-position: 0 -320px; }
+.emoji-1F1FB-1F1E6 { background-position: -20px -320px; }
+.emoji-1F1FB-1F1E8 { background-position: -40px -320px; }
+.emoji-1F1FB-1F1EA { background-position: -60px -320px; }
+.emoji-1F1FB-1F1EC { background-position: -80px -320px; }
+.emoji-1F1FB-1F1EE { background-position: -100px -320px; }
+.emoji-1F1FB-1F1F3 { background-position: -120px -320px; }
+.emoji-1F1FB-1F1FA { background-position: -140px -320px; }
+.emoji-1F1FC-1F1EB { background-position: -160px -320px; }
+.emoji-1F1FC-1F1F8 { background-position: -180px -320px; }
+.emoji-1F1FD-1F1F0 { background-position: -200px -320px; }
+.emoji-1F1FE-1F1EA { background-position: -220px -320px; }
+.emoji-1F1FE-1F1F9 { background-position: -240px -320px; }
+.emoji-1F1FF-1F1E6 { background-position: -260px -320px; }
+.emoji-1F1FF-1F1F2 { background-position: -280px -320px; }
+.emoji-1F1FF-1F1FC { background-position: -300px -320px; }
+.emoji-1F201 { background-position: -320px -320px; }
+.emoji-1F202 { background-position: -340px 0; }
+.emoji-1F21A { background-position: -340px -20px; }
+.emoji-1F22F { background-position: -340px -40px; }
+.emoji-1F232 { background-position: -340px -60px; }
+.emoji-1F233 { background-position: -340px -80px; }
+.emoji-1F234 { background-position: -340px -100px; }
+.emoji-1F235 { background-position: -340px -120px; }
+.emoji-1F236 { background-position: -340px -140px; }
+.emoji-1F237 { background-position: -340px -160px; }
+.emoji-1F238 { background-position: -340px -180px; }
+.emoji-1F239 { background-position: -340px -200px; }
+.emoji-1F23A { background-position: -340px -220px; }
+.emoji-1F250 { background-position: -340px -240px; }
+.emoji-1F251 { background-position: -340px -260px; }
+.emoji-1F300 { background-position: -340px -280px; }
+.emoji-1F301 { background-position: -340px -300px; }
+.emoji-1F302 { background-position: -340px -320px; }
+.emoji-1F303 { background-position: 0 -340px; }
+.emoji-1F304 { background-position: -20px -340px; }
+.emoji-1F305 { background-position: -40px -340px; }
+.emoji-1F306 { background-position: -60px -340px; }
+.emoji-1F307 { background-position: -80px -340px; }
+.emoji-1F308 { background-position: -100px -340px; }
+.emoji-1F309 { background-position: -120px -340px; }
+.emoji-1F30A { background-position: -140px -340px; }
+.emoji-1F30B { background-position: -160px -340px; }
+.emoji-1F30C { background-position: -180px -340px; }
+.emoji-1F30D { background-position: -200px -340px; }
+.emoji-1F30E { background-position: -220px -340px; }
+.emoji-1F30F { background-position: -240px -340px; }
+.emoji-1F310 { background-position: -260px -340px; }
+.emoji-1F311 { background-position: -280px -340px; }
+.emoji-1F312 { background-position: -300px -340px; }
+.emoji-1F313 { background-position: -320px -340px; }
+.emoji-1F314 { background-position: -340px -340px; }
+.emoji-1F315 { background-position: -360px 0; }
+.emoji-1F316 { background-position: -360px -20px; }
+.emoji-1F317 { background-position: -360px -40px; }
+.emoji-1F318 { background-position: -360px -60px; }
+.emoji-1F319 { background-position: -360px -80px; }
+.emoji-1F31A { background-position: -360px -100px; }
+.emoji-1F31B { background-position: -360px -120px; }
+.emoji-1F31C { background-position: -360px -140px; }
+.emoji-1F31D { background-position: -360px -160px; }
+.emoji-1F31E { background-position: -360px -180px; }
+.emoji-1F31F { background-position: -360px -200px; }
+.emoji-1F320 { background-position: -360px -220px; }
+.emoji-1F321 { background-position: -360px -240px; }
+.emoji-1F324 { background-position: -360px -260px; }
+.emoji-1F325 { background-position: -360px -280px; }
+.emoji-1F326 { background-position: -360px -300px; }
+.emoji-1F327 { background-position: -360px -320px; }
+.emoji-1F328 { background-position: -360px -340px; }
+.emoji-1F329 { background-position: 0 -360px; }
+.emoji-1F32A { background-position: -20px -360px; }
+.emoji-1F32B { background-position: -40px -360px; }
+.emoji-1F32C { background-position: -60px -360px; }
+.emoji-1F32D { background-position: -80px -360px; }
+.emoji-1F32E { background-position: -100px -360px; }
+.emoji-1F32F { background-position: -120px -360px; }
+.emoji-1F330 { background-position: -140px -360px; }
+.emoji-1F331 { background-position: -160px -360px; }
+.emoji-1F332 { background-position: -180px -360px; }
+.emoji-1F333 { background-position: -200px -360px; }
+.emoji-1F334 { background-position: -220px -360px; }
+.emoji-1F335 { background-position: -240px -360px; }
+.emoji-1F336 { background-position: -260px -360px; }
+.emoji-1F337 { background-position: -280px -360px; }
+.emoji-1F338 { background-position: -300px -360px; }
+.emoji-1F339 { background-position: -320px -360px; }
+.emoji-1F33A { background-position: -340px -360px; }
+.emoji-1F33B { background-position: -360px -360px; }
+.emoji-1F33C { background-position: -380px 0; }
+.emoji-1F33D { background-position: -380px -20px; }
+.emoji-1F33E { background-position: -380px -40px; }
+.emoji-1F33F { background-position: -380px -60px; }
+.emoji-1F340 { background-position: -380px -80px; }
+.emoji-1F341 { background-position: -380px -100px; }
+.emoji-1F342 { background-position: -380px -120px; }
+.emoji-1F343 { background-position: -380px -140px; }
+.emoji-1F344 { background-position: -380px -160px; }
+.emoji-1F345 { background-position: -380px -180px; }
+.emoji-1F346 { background-position: -380px -200px; }
+.emoji-1F347 { background-position: -380px -220px; }
+.emoji-1F348 { background-position: -380px -240px; }
+.emoji-1F349 { background-position: -380px -260px; }
+.emoji-1F34A { background-position: -380px -280px; }
+.emoji-1F34B { background-position: -380px -300px; }
+.emoji-1F34C { background-position: -380px -320px; }
+.emoji-1F34D { background-position: -380px -340px; }
+.emoji-1F34E { background-position: -380px -360px; }
+.emoji-1F34F { background-position: 0 -380px; }
+.emoji-1F350 { background-position: -20px -380px; }
+.emoji-1F351 { background-position: -40px -380px; }
+.emoji-1F352 { background-position: -60px -380px; }
+.emoji-1F353 { background-position: -80px -380px; }
+.emoji-1F354 { background-position: -100px -380px; }
+.emoji-1F355 { background-position: -120px -380px; }
+.emoji-1F356 { background-position: -140px -380px; }
+.emoji-1F357 { background-position: -160px -380px; }
+.emoji-1F358 { background-position: -180px -380px; }
+.emoji-1F359 { background-position: -200px -380px; }
+.emoji-1F35A { background-position: -220px -380px; }
+.emoji-1F35B { background-position: -240px -380px; }
+.emoji-1F35C { background-position: -260px -380px; }
+.emoji-1F35D { background-position: -280px -380px; }
+.emoji-1F35E { background-position: -300px -380px; }
+.emoji-1F35F { background-position: -320px -380px; }
+.emoji-1F360 { background-position: -340px -380px; }
+.emoji-1F361 { background-position: -360px -380px; }
+.emoji-1F362 { background-position: -380px -380px; }
+.emoji-1F363 { background-position: -400px 0; }
+.emoji-1F364 { background-position: -400px -20px; }
+.emoji-1F365 { background-position: -400px -40px; }
+.emoji-1F366 { background-position: -400px -60px; }
+.emoji-1F367 { background-position: -400px -80px; }
+.emoji-1F368 { background-position: -400px -100px; }
+.emoji-1F369 { background-position: -400px -120px; }
+.emoji-1F36A { background-position: -400px -140px; }
+.emoji-1F36B { background-position: -400px -160px; }
+.emoji-1F36C { background-position: -400px -180px; }
+.emoji-1F36D { background-position: -400px -200px; }
+.emoji-1F36E { background-position: -400px -220px; }
+.emoji-1F36F { background-position: -400px -240px; }
+.emoji-1F370 { background-position: -400px -260px; }
+.emoji-1F371 { background-position: -400px -280px; }
+.emoji-1F372 { background-position: -400px -300px; }
+.emoji-1F373 { background-position: -400px -320px; }
+.emoji-1F374 { background-position: -400px -340px; }
+.emoji-1F375 { background-position: -400px -360px; }
+.emoji-1F376 { background-position: -400px -380px; }
+.emoji-1F377 { background-position: 0 -400px; }
+.emoji-1F378 { background-position: -20px -400px; }
+.emoji-1F379 { background-position: -40px -400px; }
+.emoji-1F37A { background-position: -60px -400px; }
+.emoji-1F37B { background-position: -80px -400px; }
+.emoji-1F37C { background-position: -100px -400px; }
+.emoji-1F37D { background-position: -120px -400px; }
+.emoji-1F37E { background-position: -140px -400px; }
+.emoji-1F37F { background-position: -160px -400px; }
+.emoji-1F380 { background-position: -180px -400px; }
+.emoji-1F381 { background-position: -200px -400px; }
+.emoji-1F382 { background-position: -220px -400px; }
+.emoji-1F383 { background-position: -240px -400px; }
+.emoji-1F384 { background-position: -260px -400px; }
+.emoji-1F385 { background-position: -280px -400px; }
+.emoji-1F385-1F3FB { background-position: -300px -400px; }
+.emoji-1F385-1F3FC { background-position: -320px -400px; }
+.emoji-1F385-1F3FD { background-position: -340px -400px; }
+.emoji-1F385-1F3FE { background-position: -360px -400px; }
+.emoji-1F385-1F3FF { background-position: -380px -400px; }
+.emoji-1F386 { background-position: -400px -400px; }
+.emoji-1F387 { background-position: -420px 0; }
+.emoji-1F388 { background-position: -420px -20px; }
+.emoji-1F389 { background-position: -420px -40px; }
+.emoji-1F38A { background-position: -420px -60px; }
+.emoji-1F38B { background-position: -420px -80px; }
+.emoji-1F38C { background-position: -420px -100px; }
+.emoji-1F38D { background-position: -420px -120px; }
+.emoji-1F38E { background-position: -420px -140px; }
+.emoji-1F38F { background-position: -420px -160px; }
+.emoji-1F390 { background-position: -420px -180px; }
+.emoji-1F391 { background-position: -420px -200px; }
+.emoji-1F392 { background-position: -420px -220px; }
+.emoji-1F393 { background-position: -420px -240px; }
+.emoji-1F394 { background-position: -420px -260px; }
+.emoji-1F395 { background-position: -420px -280px; }
+.emoji-1F396 { background-position: -420px -300px; }
+.emoji-1F397 { background-position: -420px -320px; }
+.emoji-1F398 { background-position: -420px -340px; }
+.emoji-1F399 { background-position: -420px -360px; }
+.emoji-1F39A { background-position: -420px -380px; }
+.emoji-1F39B { background-position: -420px -400px; }
+.emoji-1F39C { background-position: 0 -420px; }
+.emoji-1F39D { background-position: -20px -420px; }
+.emoji-1F39E { background-position: -40px -420px; }
+.emoji-1F39F { background-position: -60px -420px; }
+.emoji-1F3A0 { background-position: -80px -420px; }
+.emoji-1F3A1 { background-position: -100px -420px; }
+.emoji-1F3A2 { background-position: -120px -420px; }
+.emoji-1F3A3 { background-position: -140px -420px; }
+.emoji-1F3A4 { background-position: -160px -420px; }
+.emoji-1F3A5 { background-position: -180px -420px; }
+.emoji-1F3A6 { background-position: -200px -420px; }
+.emoji-1F3A7 { background-position: -220px -420px; }
+.emoji-1F3A8 { background-position: -240px -420px; }
+.emoji-1F3A9 { background-position: -260px -420px; }
+.emoji-1F3AA { background-position: -280px -420px; }
+.emoji-1F3AB { background-position: -300px -420px; }
+.emoji-1F3AC { background-position: -320px -420px; }
+.emoji-1F3AD { background-position: -340px -420px; }
+.emoji-1F3AE { background-position: -360px -420px; }
+.emoji-1F3AF { background-position: -380px -420px; }
+.emoji-1F3B0 { background-position: -400px -420px; }
+.emoji-1F3B1 { background-position: -420px -420px; }
+.emoji-1F3B2 { background-position: -440px 0; }
+.emoji-1F3B3 { background-position: -440px -20px; }
+.emoji-1F3B4 { background-position: -440px -40px; }
+.emoji-1F3B5 { background-position: -440px -60px; }
+.emoji-1F3B6 { background-position: -440px -80px; }
+.emoji-1F3B7 { background-position: -440px -100px; }
+.emoji-1F3B8 { background-position: -440px -120px; }
+.emoji-1F3B9 { background-position: -440px -140px; }
+.emoji-1F3BA { background-position: -440px -160px; }
+.emoji-1F3BB { background-position: -440px -180px; }
+.emoji-1F3BC { background-position: -440px -200px; }
+.emoji-1F3BD { background-position: -440px -220px; }
+.emoji-1F3BE { background-position: -440px -240px; }
+.emoji-1F3BF { background-position: -440px -260px; }
+.emoji-1F3C0 { background-position: -440px -280px; }
+.emoji-1F3C1 { background-position: -440px -300px; }
+.emoji-1F3C2 { background-position: -440px -320px; }
+.emoji-1F3C3 { background-position: -440px -340px; }
+.emoji-1F3C3-1F3FB { background-position: -440px -360px; }
+.emoji-1F3C3-1F3FC { background-position: -440px -380px; }
+.emoji-1F3C3-1F3FD { background-position: -440px -400px; }
+.emoji-1F3C3-1F3FE { background-position: -440px -420px; }
+.emoji-1F3C3-1F3FF { background-position: 0 -440px; }
+.emoji-1F3C4 { background-position: -20px -440px; }
+.emoji-1F3C4-1F3FB { background-position: -40px -440px; }
+.emoji-1F3C4-1F3FC { background-position: -60px -440px; }
+.emoji-1F3C4-1F3FD { background-position: -80px -440px; }
+.emoji-1F3C4-1F3FE { background-position: -100px -440px; }
+.emoji-1F3C4-1F3FF { background-position: -120px -440px; }
+.emoji-1F3C5 { background-position: -140px -440px; }
+.emoji-1F3C6 { background-position: -160px -440px; }
+.emoji-1F3C7 { background-position: -180px -440px; }
+.emoji-1F3C7-1F3FB { background-position: -200px -440px; }
+.emoji-1F3C7-1F3FC { background-position: -220px -440px; }
+.emoji-1F3C7-1F3FD { background-position: -240px -440px; }
+.emoji-1F3C7-1F3FE { background-position: -260px -440px; }
+.emoji-1F3C7-1F3FF { background-position: -280px -440px; }
+.emoji-1F3C8 { background-position: -300px -440px; }
+.emoji-1F3C9 { background-position: -320px -440px; }
+.emoji-1F3CA { background-position: -340px -440px; }
+.emoji-1F3CA-1F3FB { background-position: -360px -440px; }
+.emoji-1F3CA-1F3FC { background-position: -380px -440px; }
+.emoji-1F3CA-1F3FD { background-position: -400px -440px; }
+.emoji-1F3CA-1F3FE { background-position: -420px -440px; }
+.emoji-1F3CA-1F3FF { background-position: -440px -440px; }
+.emoji-1F3CB { background-position: -460px 0; }
+.emoji-1F3CB-1F3FB { background-position: -460px -20px; }
+.emoji-1F3CB-1F3FC { background-position: -460px -40px; }
+.emoji-1F3CB-1F3FD { background-position: -460px -60px; }
+.emoji-1F3CB-1F3FE { background-position: -460px -80px; }
+.emoji-1F3CB-1F3FF { background-position: -460px -100px; }
+.emoji-1F3CC { background-position: -460px -120px; }
+.emoji-1F3CD { background-position: -460px -140px; }
+.emoji-1F3CE { background-position: -460px -160px; }
+.emoji-1F3CF { background-position: -460px -180px; }
+.emoji-1F3D0 { background-position: -460px -200px; }
+.emoji-1F3D1 { background-position: -460px -220px; }
+.emoji-1F3D2 { background-position: -460px -240px; }
+.emoji-1F3D3 { background-position: -460px -260px; }
+.emoji-1F3D4 { background-position: -460px -280px; }
+.emoji-1F3D5 { background-position: -460px -300px; }
+.emoji-1F3D6 { background-position: -460px -320px; }
+.emoji-1F3D7 { background-position: -460px -340px; }
+.emoji-1F3D8 { background-position: -460px -360px; }
+.emoji-1F3D9 { background-position: -460px -380px; }
+.emoji-1F3DA { background-position: -460px -400px; }
+.emoji-1F3DB { background-position: -460px -420px; }
+.emoji-1F3DC { background-position: -460px -440px; }
+.emoji-1F3DD { background-position: 0 -460px; }
+.emoji-1F3DE { background-position: -20px -460px; }
+.emoji-1F3DF { background-position: -40px -460px; }
+.emoji-1F3E0 { background-position: -60px -460px; }
+.emoji-1F3E1 { background-position: -80px -460px; }
+.emoji-1F3E2 { background-position: -100px -460px; }
+.emoji-1F3E3 { background-position: -120px -460px; }
+.emoji-1F3E4 { background-position: -140px -460px; }
+.emoji-1F3E5 { background-position: -160px -460px; }
+.emoji-1F3E6 { background-position: -180px -460px; }
+.emoji-1F3E7 { background-position: -200px -460px; }
+.emoji-1F3E8 { background-position: -220px -460px; }
+.emoji-1F3E9 { background-position: -240px -460px; }
+.emoji-1F3EA { background-position: -260px -460px; }
+.emoji-1F3EB { background-position: -280px -460px; }
+.emoji-1F3EC { background-position: -300px -460px; }
+.emoji-1F3ED { background-position: -320px -460px; }
+.emoji-1F3EE { background-position: -340px -460px; }
+.emoji-1F3EF { background-position: -360px -460px; }
+.emoji-1F3F0 { background-position: -380px -460px; }
+.emoji-1F3F1 { background-position: -400px -460px; }
+.emoji-1F3F2 { background-position: -420px -460px; }
+.emoji-1F3F3 { background-position: -440px -460px; }
+.emoji-1F3F4 { background-position: -460px -460px; }
+.emoji-1F3F5 { background-position: -480px 0; }
+.emoji-1F3F6 { background-position: -480px -20px; }
+.emoji-1F3F7 { background-position: -480px -40px; }
+.emoji-1F3F8 { background-position: -480px -60px; }
+.emoji-1F3F9 { background-position: -480px -80px; }
+.emoji-1F3FA { background-position: -480px -100px; }
+.emoji-1F3FB { background-position: -480px -120px; }
+.emoji-1F3FC { background-position: -480px -140px; }
+.emoji-1F3FD { background-position: -480px -160px; }
+.emoji-1F3FE { background-position: -480px -180px; }
+.emoji-1F3FF { background-position: -480px -200px; }
+.emoji-1F400 { background-position: -480px -220px; }
+.emoji-1F401 { background-position: -480px -240px; }
+.emoji-1F402 { background-position: -480px -260px; }
+.emoji-1F403 { background-position: -480px -280px; }
+.emoji-1F404 { background-position: -480px -300px; }
+.emoji-1F405 { background-position: -480px -320px; }
+.emoji-1F406 { background-position: -480px -340px; }
+.emoji-1F407 { background-position: -480px -360px; }
+.emoji-1F408 { background-position: -480px -380px; }
+.emoji-1F409 { background-position: -480px -400px; }
+.emoji-1F40A { background-position: -480px -420px; }
+.emoji-1F40B { background-position: -480px -440px; }
+.emoji-1F40C { background-position: -480px -460px; }
+.emoji-1F40D { background-position: 0 -480px; }
+.emoji-1F40E { background-position: -20px -480px; }
+.emoji-1F40F { background-position: -40px -480px; }
+.emoji-1F410 { background-position: -60px -480px; }
+.emoji-1F411 { background-position: -80px -480px; }
+.emoji-1F412 { background-position: -100px -480px; }
+.emoji-1F413 { background-position: -120px -480px; }
+.emoji-1F414 { background-position: -140px -480px; }
+.emoji-1F415 { background-position: -160px -480px; }
+.emoji-1F416 { background-position: -180px -480px; }
+.emoji-1F417 { background-position: -200px -480px; }
+.emoji-1F418 { background-position: -220px -480px; }
+.emoji-1F419 { background-position: -240px -480px; }
+.emoji-1F41A { background-position: -260px -480px; }
+.emoji-1F41B { background-position: -280px -480px; }
+.emoji-1F41C { background-position: -300px -480px; }
+.emoji-1F41D { background-position: -320px -480px; }
+.emoji-1F41E { background-position: -340px -480px; }
+.emoji-1F41F { background-position: -360px -480px; }
+.emoji-1F420 { background-position: -380px -480px; }
+.emoji-1F421 { background-position: -400px -480px; }
+.emoji-1F422 { background-position: -420px -480px; }
+.emoji-1F423 { background-position: -440px -480px; }
+.emoji-1F424 { background-position: -460px -480px; }
+.emoji-1F425 { background-position: -480px -480px; }
+.emoji-1F426 { background-position: -500px 0; }
+.emoji-1F427 { background-position: -500px -20px; }
+.emoji-1F428 { background-position: -500px -40px; }
+.emoji-1F429 { background-position: -500px -60px; }
+.emoji-1F42A { background-position: -500px -80px; }
+.emoji-1F42B { background-position: -500px -100px; }
+.emoji-1F42C { background-position: -500px -120px; }
+.emoji-1F42D { background-position: -500px -140px; }
+.emoji-1F42E { background-position: -500px -160px; }
+.emoji-1F42F { background-position: -500px -180px; }
+.emoji-1F430 { background-position: -500px -200px; }
+.emoji-1F431 { background-position: -500px -220px; }
+.emoji-1F432 { background-position: -500px -240px; }
+.emoji-1F433 { background-position: -500px -260px; }
+.emoji-1F434 { background-position: -500px -280px; }
+.emoji-1F435 { background-position: -500px -300px; }
+.emoji-1F436 { background-position: -500px -320px; }
+.emoji-1F437 { background-position: -500px -340px; }
+.emoji-1F438 { background-position: -500px -360px; }
+.emoji-1F439 { background-position: -500px -380px; }
+.emoji-1F43A { background-position: -500px -400px; }
+.emoji-1F43B { background-position: -500px -420px; }
+.emoji-1F43C { background-position: -500px -440px; }
+.emoji-1F43D { background-position: -500px -460px; }
+.emoji-1F43E { background-position: -500px -480px; }
+.emoji-1F43F { background-position: 0 -500px; }
+.emoji-1F440 { background-position: -20px -500px; }
+.emoji-1F441 { background-position: -40px -500px; }
+.emoji-1F441-1F5E8 { background-position: -60px -500px; }
+.emoji-1F442 { background-position: -80px -500px; }
+.emoji-1F442-1F3FB { background-position: -100px -500px; }
+.emoji-1F442-1F3FC { background-position: -120px -500px; }
+.emoji-1F442-1F3FD { background-position: -140px -500px; }
+.emoji-1F442-1F3FE { background-position: -160px -500px; }
+.emoji-1F442-1F3FF { background-position: -180px -500px; }
+.emoji-1F443 { background-position: -200px -500px; }
+.emoji-1F443-1F3FB { background-position: -220px -500px; }
+.emoji-1F443-1F3FC { background-position: -240px -500px; }
+.emoji-1F443-1F3FD { background-position: -260px -500px; }
+.emoji-1F443-1F3FE { background-position: -280px -500px; }
+.emoji-1F443-1F3FF { background-position: -300px -500px; }
+.emoji-1F444 { background-position: -320px -500px; }
+.emoji-1F445 { background-position: -340px -500px; }
+.emoji-1F446 { background-position: -360px -500px; }
+.emoji-1F446-1F3FB { background-position: -380px -500px; }
+.emoji-1F446-1F3FC { background-position: -400px -500px; }
+.emoji-1F446-1F3FD { background-position: -420px -500px; }
+.emoji-1F446-1F3FE { background-position: -440px -500px; }
+.emoji-1F446-1F3FF { background-position: -460px -500px; }
+.emoji-1F447 { background-position: -480px -500px; }
+.emoji-1F447-1F3FB { background-position: -500px -500px; }
+.emoji-1F447-1F3FC { background-position: -520px 0; }
+.emoji-1F447-1F3FD { background-position: -520px -20px; }
+.emoji-1F447-1F3FE { background-position: -520px -40px; }
+.emoji-1F447-1F3FF { background-position: -520px -60px; }
+.emoji-1F448 { background-position: -520px -80px; }
+.emoji-1F448-1F3FB { background-position: -520px -100px; }
+.emoji-1F448-1F3FC { background-position: -520px -120px; }
+.emoji-1F448-1F3FD { background-position: -520px -140px; }
+.emoji-1F448-1F3FE { background-position: -520px -160px; }
+.emoji-1F448-1F3FF { background-position: -520px -180px; }
+.emoji-1F449 { background-position: -520px -200px; }
+.emoji-1F449-1F3FB { background-position: -520px -220px; }
+.emoji-1F449-1F3FC { background-position: -520px -240px; }
+.emoji-1F449-1F3FD { background-position: -520px -260px; }
+.emoji-1F449-1F3FE { background-position: -520px -280px; }
+.emoji-1F449-1F3FF { background-position: -520px -300px; }
+.emoji-1F44A { background-position: -520px -320px; }
+.emoji-1F44A-1F3FB { background-position: -520px -340px; }
+.emoji-1F44A-1F3FC { background-position: -520px -360px; }
+.emoji-1F44A-1F3FD { background-position: -520px -380px; }
+.emoji-1F44A-1F3FE { background-position: -520px -400px; }
+.emoji-1F44A-1F3FF { background-position: -520px -420px; }
+.emoji-1F44B { background-position: -520px -440px; }
+.emoji-1F44B-1F3FB { background-position: -520px -460px; }
+.emoji-1F44B-1F3FC { background-position: -520px -480px; }
+.emoji-1F44B-1F3FD { background-position: -520px -500px; }
+.emoji-1F44B-1F3FE { background-position: 0 -520px; }
+.emoji-1F44B-1F3FF { background-position: -20px -520px; }
+.emoji-1F44C { background-position: -40px -520px; }
+.emoji-1F44C-1F3FB { background-position: -60px -520px; }
+.emoji-1F44C-1F3FC { background-position: -80px -520px; }
+.emoji-1F44C-1F3FD { background-position: -100px -520px; }
+.emoji-1F44C-1F3FE { background-position: -120px -520px; }
+.emoji-1F44C-1F3FF { background-position: -140px -520px; }
+.emoji-1F44D { background-position: -160px -520px; }
+.emoji-1F44D-1F3FB { background-position: -180px -520px; }
+.emoji-1F44D-1F3FC { background-position: -200px -520px; }
+.emoji-1F44D-1F3FD { background-position: -220px -520px; }
+.emoji-1F44D-1F3FE { background-position: -240px -520px; }
+.emoji-1F44D-1F3FF { background-position: -260px -520px; }
+.emoji-1F44E { background-position: -280px -520px; }
+.emoji-1F44E-1F3FB { background-position: -300px -520px; }
+.emoji-1F44E-1F3FC { background-position: -320px -520px; }
+.emoji-1F44E-1F3FD { background-position: -340px -520px; }
+.emoji-1F44E-1F3FE { background-position: -360px -520px; }
+.emoji-1F44E-1F3FF { background-position: -380px -520px; }
+.emoji-1F44F { background-position: -400px -520px; }
+.emoji-1F44F-1F3FB { background-position: -420px -520px; }
+.emoji-1F44F-1F3FC { background-position: -440px -520px; }
+.emoji-1F44F-1F3FD { background-position: -460px -520px; }
+.emoji-1F44F-1F3FE { background-position: -480px -520px; }
+.emoji-1F44F-1F3FF { background-position: -500px -520px; }
+.emoji-1F450 { background-position: -520px -520px; }
+.emoji-1F450-1F3FB { background-position: -540px 0; }
+.emoji-1F450-1F3FC { background-position: -540px -20px; }
+.emoji-1F450-1F3FD { background-position: -540px -40px; }
+.emoji-1F450-1F3FE { background-position: -540px -60px; }
+.emoji-1F450-1F3FF { background-position: -540px -80px; }
+.emoji-1F451 { background-position: -540px -100px; }
+.emoji-1F452 { background-position: -540px -120px; }
+.emoji-1F453 { background-position: -540px -140px; }
+.emoji-1F454 { background-position: -540px -160px; }
+.emoji-1F455 { background-position: -540px -180px; }
+.emoji-1F456 { background-position: -540px -200px; }
+.emoji-1F457 { background-position: -540px -220px; }
+.emoji-1F458 { background-position: -540px -240px; }
+.emoji-1F459 { background-position: -540px -260px; }
+.emoji-1F45A { background-position: -540px -280px; }
+.emoji-1F45B { background-position: -540px -300px; }
+.emoji-1F45C { background-position: -540px -320px; }
+.emoji-1F45D { background-position: -540px -340px; }
+.emoji-1F45E { background-position: -540px -360px; }
+.emoji-1F45F { background-position: -540px -380px; }
+.emoji-1F460 { background-position: -540px -400px; }
+.emoji-1F461 { background-position: -540px -420px; }
+.emoji-1F462 { background-position: -540px -440px; }
+.emoji-1F463 { background-position: -540px -460px; }
+.emoji-1F464 { background-position: -540px -480px; }
+.emoji-1F465 { background-position: -540px -500px; }
+.emoji-1F466 { background-position: -540px -520px; }
+.emoji-1F466-1F3FB { background-position: 0 -540px; }
+.emoji-1F466-1F3FC { background-position: -20px -540px; }
+.emoji-1F466-1F3FD { background-position: -40px -540px; }
+.emoji-1F466-1F3FE { background-position: -60px -540px; }
+.emoji-1F466-1F3FF { background-position: -80px -540px; }
+.emoji-1F467 { background-position: -100px -540px; }
+.emoji-1F467-1F3FB { background-position: -120px -540px; }
+.emoji-1F467-1F3FC { background-position: -140px -540px; }
+.emoji-1F467-1F3FD { background-position: -160px -540px; }
+.emoji-1F467-1F3FE { background-position: -180px -540px; }
+.emoji-1F467-1F3FF { background-position: -200px -540px; }
+.emoji-1F468 { background-position: -220px -540px; }
+.emoji-1F468-1F3FB { background-position: -240px -540px; }
+.emoji-1F468-1F3FC { background-position: -260px -540px; }
+.emoji-1F468-1F3FD { background-position: -280px -540px; }
+.emoji-1F468-1F3FE { background-position: -300px -540px; }
+.emoji-1F468-1F3FF { background-position: -320px -540px; }
+.emoji-1F468-1F468-1F466 { background-position: -340px -540px; }
+.emoji-1F468-1F468-1F466-1F466 { background-position: -360px -540px; }
+.emoji-1F468-1F468-1F467 { background-position: -380px -540px; }
+.emoji-1F468-1F468-1F467-1F466 { background-position: -400px -540px; }
+.emoji-1F468-1F468-1F467-1F467 { background-position: -420px -540px; }
+.emoji-1F468-1F469-1F466-1F466 { background-position: -440px -540px; }
+.emoji-1F468-1F469-1F467 { background-position: -460px -540px; }
+.emoji-1F468-1F469-1F467-1F466 { background-position: -480px -540px; }
+.emoji-1F468-1F469-1F467-1F467 { background-position: -500px -540px; }
+.emoji-1F468-2764-1F468 { background-position: -520px -540px; }
+.emoji-1F468-2764-1F48B-1F468 { background-position: -540px -540px; }
+.emoji-1F469 { background-position: -560px 0; }
+.emoji-1F469-1F3FB { background-position: -560px -20px; }
+.emoji-1F469-1F3FC { background-position: -560px -40px; }
+.emoji-1F469-1F3FD { background-position: -560px -60px; }
+.emoji-1F469-1F3FE { background-position: -560px -80px; }
+.emoji-1F469-1F3FF { background-position: -560px -100px; }
+.emoji-1F469-1F469-1F466 { background-position: -560px -120px; }
+.emoji-1F469-1F469-1F466-1F466 { background-position: -560px -140px; }
+.emoji-1F469-1F469-1F467 { background-position: -560px -160px; }
+.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -180px; }
+.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -200px; }
+.emoji-1F469-2764-1F469 { background-position: -560px -220px; }
+.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -240px; }
+.emoji-1F46A { background-position: -560px -260px; }
+.emoji-1F46B { background-position: -560px -280px; }
+.emoji-1F46C { background-position: -560px -300px; }
+.emoji-1F46D { background-position: -560px -320px; }
+.emoji-1F46E { background-position: -560px -340px; }
+.emoji-1F46E-1F3FB { background-position: -560px -360px; }
+.emoji-1F46E-1F3FC { background-position: -560px -380px; }
+.emoji-1F46E-1F3FD { background-position: -560px -400px; }
+.emoji-1F46E-1F3FE { background-position: -560px -420px; }
+.emoji-1F46E-1F3FF { background-position: -560px -440px; }
+.emoji-1F46F { background-position: -560px -460px; }
+.emoji-1F470 { background-position: -560px -480px; }
+.emoji-1F470-1F3FB { background-position: -560px -500px; }
+.emoji-1F470-1F3FC { background-position: -560px -520px; }
+.emoji-1F470-1F3FD { background-position: -560px -540px; }
+.emoji-1F470-1F3FE { background-position: 0 -560px; }
+.emoji-1F470-1F3FF { background-position: -20px -560px; }
+.emoji-1F471 { background-position: -40px -560px; }
+.emoji-1F471-1F3FB { background-position: -60px -560px; }
+.emoji-1F471-1F3FC { background-position: -80px -560px; }
+.emoji-1F471-1F3FD { background-position: -100px -560px; }
+.emoji-1F471-1F3FE { background-position: -120px -560px; }
+.emoji-1F471-1F3FF { background-position: -140px -560px; }
+.emoji-1F472 { background-position: -160px -560px; }
+.emoji-1F472-1F3FB { background-position: -180px -560px; }
+.emoji-1F472-1F3FC { background-position: -200px -560px; }
+.emoji-1F472-1F3FD { background-position: -220px -560px; }
+.emoji-1F472-1F3FE { background-position: -240px -560px; }
+.emoji-1F472-1F3FF { background-position: -260px -560px; }
+.emoji-1F473 { background-position: -280px -560px; }
+.emoji-1F473-1F3FB { background-position: -300px -560px; }
+.emoji-1F473-1F3FC { background-position: -320px -560px; }
+.emoji-1F473-1F3FD { background-position: -340px -560px; }
+.emoji-1F473-1F3FE { background-position: -360px -560px; }
+.emoji-1F473-1F3FF { background-position: -380px -560px; }
+.emoji-1F474 { background-position: -400px -560px; }
+.emoji-1F474-1F3FB { background-position: -420px -560px; }
+.emoji-1F474-1F3FC { background-position: -440px -560px; }
+.emoji-1F474-1F3FD { background-position: -460px -560px; }
+.emoji-1F474-1F3FE { background-position: -480px -560px; }
+.emoji-1F474-1F3FF { background-position: -500px -560px; }
+.emoji-1F475 { background-position: -520px -560px; }
+.emoji-1F475-1F3FB { background-position: -540px -560px; }
+.emoji-1F475-1F3FC { background-position: -560px -560px; }
+.emoji-1F475-1F3FD { background-position: -580px 0; }
+.emoji-1F475-1F3FE { background-position: -580px -20px; }
+.emoji-1F475-1F3FF { background-position: -580px -40px; }
+.emoji-1F476 { background-position: -580px -60px; }
+.emoji-1F476-1F3FB { background-position: -580px -80px; }
+.emoji-1F476-1F3FC { background-position: -580px -100px; }
+.emoji-1F476-1F3FD { background-position: -580px -120px; }
+.emoji-1F476-1F3FE { background-position: -580px -140px; }
+.emoji-1F476-1F3FF { background-position: -580px -160px; }
+.emoji-1F477 { background-position: -580px -180px; }
+.emoji-1F477-1F3FB { background-position: -580px -200px; }
+.emoji-1F477-1F3FC { background-position: -580px -220px; }
+.emoji-1F477-1F3FD { background-position: -580px -240px; }
+.emoji-1F477-1F3FE { background-position: -580px -260px; }
+.emoji-1F477-1F3FF { background-position: -580px -280px; }
+.emoji-1F478 { background-position: -580px -300px; }
+.emoji-1F478-1F3FB { background-position: -580px -320px; }
+.emoji-1F478-1F3FC { background-position: -580px -340px; }
+.emoji-1F478-1F3FD { background-position: -580px -360px; }
+.emoji-1F478-1F3FE { background-position: -580px -380px; }
+.emoji-1F478-1F3FF { background-position: -580px -400px; }
+.emoji-1F479 { background-position: -580px -420px; }
+.emoji-1F47A { background-position: -580px -440px; }
+.emoji-1F47B { background-position: -580px -460px; }
+.emoji-1F47C { background-position: -580px -480px; }
+.emoji-1F47C-1F3FB { background-position: -580px -500px; }
+.emoji-1F47C-1F3FC { background-position: -580px -520px; }
+.emoji-1F47C-1F3FD { background-position: -580px -540px; }
+.emoji-1F47C-1F3FE { background-position: -580px -560px; }
+.emoji-1F47C-1F3FF { background-position: 0 -580px; }
+.emoji-1F47D { background-position: -20px -580px; }
+.emoji-1F47E { background-position: -40px -580px; }
+.emoji-1F47F { background-position: -60px -580px; }
+.emoji-1F480 { background-position: -80px -580px; }
+.emoji-1F481 { background-position: -100px -580px; }
+.emoji-1F481-1F3FB { background-position: -120px -580px; }
+.emoji-1F481-1F3FC { background-position: -140px -580px; }
+.emoji-1F481-1F3FD { background-position: -160px -580px; }
+.emoji-1F481-1F3FE { background-position: -180px -580px; }
+.emoji-1F481-1F3FF { background-position: -200px -580px; }
+.emoji-1F482 { background-position: -220px -580px; }
+.emoji-1F482-1F3FB { background-position: -240px -580px; }
+.emoji-1F482-1F3FC { background-position: -260px -580px; }
+.emoji-1F482-1F3FD { background-position: -280px -580px; }
+.emoji-1F482-1F3FE { background-position: -300px -580px; }
+.emoji-1F482-1F3FF { background-position: -320px -580px; }
+.emoji-1F483 { background-position: -340px -580px; }
+.emoji-1F483-1F3FB { background-position: -360px -580px; }
+.emoji-1F483-1F3FC { background-position: -380px -580px; }
+.emoji-1F483-1F3FD { background-position: -400px -580px; }
+.emoji-1F483-1F3FE { background-position: -420px -580px; }
+.emoji-1F483-1F3FF { background-position: -440px -580px; }
+.emoji-1F484 { background-position: -460px -580px; }
+.emoji-1F485 { background-position: -480px -580px; }
+.emoji-1F485-1F3FB { background-position: -500px -580px; }
+.emoji-1F485-1F3FC { background-position: -520px -580px; }
+.emoji-1F485-1F3FD { background-position: -540px -580px; }
+.emoji-1F485-1F3FE { background-position: -560px -580px; }
+.emoji-1F485-1F3FF { background-position: -580px -580px; }
+.emoji-1F486 { background-position: -600px 0; }
+.emoji-1F486-1F3FB { background-position: -600px -20px; }
+.emoji-1F486-1F3FC { background-position: -600px -40px; }
+.emoji-1F486-1F3FD { background-position: -600px -60px; }
+.emoji-1F486-1F3FE { background-position: -600px -80px; }
+.emoji-1F486-1F3FF { background-position: -600px -100px; }
+.emoji-1F487 { background-position: -600px -120px; }
+.emoji-1F487-1F3FB { background-position: -600px -140px; }
+.emoji-1F487-1F3FC { background-position: -600px -160px; }
+.emoji-1F487-1F3FD { background-position: -600px -180px; }
+.emoji-1F487-1F3FE { background-position: -600px -200px; }
+.emoji-1F487-1F3FF { background-position: -600px -220px; }
+.emoji-1F488 { background-position: -600px -240px; }
+.emoji-1F489 { background-position: -600px -260px; }
+.emoji-1F48A { background-position: -600px -280px; }
+.emoji-1F48B { background-position: -600px -300px; }
+.emoji-1F48C { background-position: -600px -320px; }
+.emoji-1F48D { background-position: -600px -340px; }
+.emoji-1F48E { background-position: -600px -360px; }
+.emoji-1F48F { background-position: -600px -380px; }
+.emoji-1F490 { background-position: -600px -400px; }
+.emoji-1F491 { background-position: -600px -420px; }
+.emoji-1F492 { background-position: -600px -440px; }
+.emoji-1F493 { background-position: -600px -460px; }
+.emoji-1F494 { background-position: -600px -480px; }
+.emoji-1F495 { background-position: -600px -500px; }
+.emoji-1F496 { background-position: -600px -520px; }
+.emoji-1F497 { background-position: -600px -540px; }
+.emoji-1F498 { background-position: -600px -560px; }
+.emoji-1F499 { background-position: -600px -580px; }
+.emoji-1F49A { background-position: 0 -600px; }
+.emoji-1F49B { background-position: -20px -600px; }
+.emoji-1F49C { background-position: -40px -600px; }
+.emoji-1F49D { background-position: -60px -600px; }
+.emoji-1F49E { background-position: -80px -600px; }
+.emoji-1F49F { background-position: -100px -600px; }
+.emoji-1F4A0 { background-position: -120px -600px; }
+.emoji-1F4A1 { background-position: -140px -600px; }
+.emoji-1F4A2 { background-position: -160px -600px; }
+.emoji-1F4A3 { background-position: -180px -600px; }
+.emoji-1F4A4 { background-position: -200px -600px; }
+.emoji-1F4A5 { background-position: -220px -600px; }
+.emoji-1F4A6 { background-position: -240px -600px; }
+.emoji-1F4A7 { background-position: -260px -600px; }
+.emoji-1F4A8 { background-position: -280px -600px; }
+.emoji-1F4A9 { background-position: -300px -600px; }
+.emoji-1F4AA { background-position: -320px -600px; }
+.emoji-1F4AA-1F3FB { background-position: -340px -600px; }
+.emoji-1F4AA-1F3FC { background-position: -360px -600px; }
+.emoji-1F4AA-1F3FD { background-position: -380px -600px; }
+.emoji-1F4AA-1F3FE { background-position: -400px -600px; }
+.emoji-1F4AA-1F3FF { background-position: -420px -600px; }
+.emoji-1F4AB { background-position: -440px -600px; }
+.emoji-1F4AC { background-position: -460px -600px; }
+.emoji-1F4AD { background-position: -480px -600px; }
+.emoji-1F4AE { background-position: -500px -600px; }
+.emoji-1F4AF { background-position: -520px -600px; }
+.emoji-1F4B0 { background-position: -540px -600px; }
+.emoji-1F4B1 { background-position: -560px -600px; }
+.emoji-1F4B2 { background-position: -580px -600px; }
+.emoji-1F4B3 { background-position: -600px -600px; }
+.emoji-1F4B4 { background-position: -620px 0; }
+.emoji-1F4B5 { background-position: -620px -20px; }
+.emoji-1F4B6 { background-position: -620px -40px; }
+.emoji-1F4B7 { background-position: -620px -60px; }
+.emoji-1F4B8 { background-position: -620px -80px; }
+.emoji-1F4B9 { background-position: -620px -100px; }
+.emoji-1F4BA { background-position: -620px -120px; }
+.emoji-1F4BB { background-position: -620px -140px; }
+.emoji-1F4BC { background-position: -620px -160px; }
+.emoji-1F4BD { background-position: -620px -180px; }
+.emoji-1F4BE { background-position: -620px -200px; }
+.emoji-1F4BF { background-position: -620px -220px; }
+.emoji-1F4C0 { background-position: -620px -240px; }
+.emoji-1F4C1 { background-position: -620px -260px; }
+.emoji-1F4C2 { background-position: -620px -280px; }
+.emoji-1F4C3 { background-position: -620px -300px; }
+.emoji-1F4C4 { background-position: -620px -320px; }
+.emoji-1F4C5 { background-position: -620px -340px; }
+.emoji-1F4C6 { background-position: -620px -360px; }
+.emoji-1F4C7 { background-position: -620px -380px; }
+.emoji-1F4C8 { background-position: -620px -400px; }
+.emoji-1F4C9 { background-position: -620px -420px; }
+.emoji-1F4CA { background-position: -620px -440px; }
+.emoji-1F4CB { background-position: -620px -460px; }
+.emoji-1F4CC { background-position: -620px -480px; }
+.emoji-1F4CD { background-position: -620px -500px; }
+.emoji-1F4CE { background-position: -620px -520px; }
+.emoji-1F4CF { background-position: -620px -540px; }
+.emoji-1F4D0 { background-position: -620px -560px; }
+.emoji-1F4D1 { background-position: -620px -580px; }
+.emoji-1F4D2 { background-position: -620px -600px; }
+.emoji-1F4D3 { background-position: 0 -620px; }
+.emoji-1F4D4 { background-position: -20px -620px; }
+.emoji-1F4D5 { background-position: -40px -620px; }
+.emoji-1F4D6 { background-position: -60px -620px; }
+.emoji-1F4D7 { background-position: -80px -620px; }
+.emoji-1F4D8 { background-position: -100px -620px; }
+.emoji-1F4D9 { background-position: -120px -620px; }
+.emoji-1F4DA { background-position: -140px -620px; }
+.emoji-1F4DB { background-position: -160px -620px; }
+.emoji-1F4DC { background-position: -180px -620px; }
+.emoji-1F4DD { background-position: -200px -620px; }
+.emoji-1F4DE { background-position: -220px -620px; }
+.emoji-1F4DF { background-position: -240px -620px; }
+.emoji-1F4E0 { background-position: -260px -620px; }
+.emoji-1F4E1 { background-position: -280px -620px; }
+.emoji-1F4E2 { background-position: -300px -620px; }
+.emoji-1F4E3 { background-position: -320px -620px; }
+.emoji-1F4E4 { background-position: -340px -620px; }
+.emoji-1F4E5 { background-position: -360px -620px; }
+.emoji-1F4E6 { background-position: -380px -620px; }
+.emoji-1F4E7 { background-position: -400px -620px; }
+.emoji-1F4E8 { background-position: -420px -620px; }
+.emoji-1F4E9 { background-position: -440px -620px; }
+.emoji-1F4EA { background-position: -460px -620px; }
+.emoji-1F4EB { background-position: -480px -620px; }
+.emoji-1F4EC { background-position: -500px -620px; }
+.emoji-1F4ED { background-position: -520px -620px; }
+.emoji-1F4EE { background-position: -540px -620px; }
+.emoji-1F4EF { background-position: -560px -620px; }
+.emoji-1F4F0 { background-position: -580px -620px; }
+.emoji-1F4F1 { background-position: -600px -620px; }
+.emoji-1F4F2 { background-position: -620px -620px; }
+.emoji-1F4F3 { background-position: -640px 0; }
+.emoji-1F4F4 { background-position: -640px -20px; }
+.emoji-1F4F5 { background-position: -640px -40px; }
+.emoji-1F4F6 { background-position: -640px -60px; }
+.emoji-1F4F7 { background-position: -640px -80px; }
+.emoji-1F4F8 { background-position: -640px -100px; }
+.emoji-1F4F9 { background-position: -640px -120px; }
+.emoji-1F4FA { background-position: -640px -140px; }
+.emoji-1F4FB { background-position: -640px -160px; }
+.emoji-1F4FC { background-position: -640px -180px; }
+.emoji-1F4FD { background-position: -640px -200px; }
+.emoji-1F4FE { background-position: -640px -220px; }
+.emoji-1F4FF { background-position: -640px -240px; }
+.emoji-1F500 { background-position: -640px -260px; }
+.emoji-1F501 { background-position: -640px -280px; }
+.emoji-1F502 { background-position: -640px -300px; }
+.emoji-1F503 { background-position: -640px -320px; }
+.emoji-1F504 { background-position: -640px -340px; }
+.emoji-1F505 { background-position: -640px -360px; }
+.emoji-1F506 { background-position: -640px -380px; }
+.emoji-1F507 { background-position: -640px -400px; }
+.emoji-1F508 { background-position: -640px -420px; }
+.emoji-1F509 { background-position: -640px -440px; }
+.emoji-1F50A { background-position: -640px -460px; }
+.emoji-1F50B { background-position: -640px -480px; }
+.emoji-1F50C { background-position: -640px -500px; }
+.emoji-1F50D { background-position: -640px -520px; }
+.emoji-1F50E { background-position: -640px -540px; }
+.emoji-1F50F { background-position: -640px -560px; }
+.emoji-1F510 { background-position: -640px -580px; }
+.emoji-1F511 { background-position: -640px -600px; }
+.emoji-1F512 { background-position: -640px -620px; }
+.emoji-1F513 { background-position: 0 -640px; }
+.emoji-1F514 { background-position: -20px -640px; }
+.emoji-1F515 { background-position: -40px -640px; }
+.emoji-1F516 { background-position: -60px -640px; }
+.emoji-1F517 { background-position: -80px -640px; }
+.emoji-1F518 { background-position: -100px -640px; }
+.emoji-1F519 { background-position: -120px -640px; }
+.emoji-1F51A { background-position: -140px -640px; }
+.emoji-1F51B { background-position: -160px -640px; }
+.emoji-1F51C { background-position: -180px -640px; }
+.emoji-1F51D { background-position: -200px -640px; }
+.emoji-1F51E { background-position: -220px -640px; }
+.emoji-1F51F { background-position: -240px -640px; }
+.emoji-1F520 { background-position: -260px -640px; }
+.emoji-1F521 { background-position: -280px -640px; }
+.emoji-1F522 { background-position: -300px -640px; }
+.emoji-1F523 { background-position: -320px -640px; }
+.emoji-1F524 { background-position: -340px -640px; }
+.emoji-1F525 { background-position: -360px -640px; }
+.emoji-1F526 { background-position: -380px -640px; }
+.emoji-1F527 { background-position: -400px -640px; }
+.emoji-1F528 { background-position: -420px -640px; }
+.emoji-1F529 { background-position: -440px -640px; }
+.emoji-1F52A { background-position: -460px -640px; }
+.emoji-1F52B { background-position: -480px -640px; }
+.emoji-1F52C { background-position: -500px -640px; }
+.emoji-1F52D { background-position: -520px -640px; }
+.emoji-1F52E { background-position: -540px -640px; }
+.emoji-1F52F { background-position: -560px -640px; }
+.emoji-1F530 { background-position: -580px -640px; }
+.emoji-1F531 { background-position: -600px -640px; }
+.emoji-1F532 { background-position: -620px -640px; }
+.emoji-1F533 { background-position: -640px -640px; }
+.emoji-1F534 { background-position: -660px 0; }
+.emoji-1F535 { background-position: -660px -20px; }
+.emoji-1F536 { background-position: -660px -40px; }
+.emoji-1F537 { background-position: -660px -60px; }
+.emoji-1F538 { background-position: -660px -80px; }
+.emoji-1F539 { background-position: -660px -100px; }
+.emoji-1F53A { background-position: -660px -120px; }
+.emoji-1F53B { background-position: -660px -140px; }
+.emoji-1F53C { background-position: -660px -160px; }
+.emoji-1F53D { background-position: -660px -180px; }
+.emoji-1F546 { background-position: -660px -200px; }
+.emoji-1F547 { background-position: -660px -220px; }
+.emoji-1F548 { background-position: -660px -240px; }
+.emoji-1F549 { background-position: -660px -260px; }
+.emoji-1F54A { background-position: -660px -280px; }
+.emoji-1F54B { background-position: -660px -300px; }
+.emoji-1F54C { background-position: -660px -320px; }
+.emoji-1F54D { background-position: -660px -340px; }
+.emoji-1F54E { background-position: -660px -360px; }
+.emoji-1F550 { background-position: -660px -380px; }
+.emoji-1F551 { background-position: -660px -400px; }
+.emoji-1F552 { background-position: -660px -420px; }
+.emoji-1F553 { background-position: -660px -440px; }
+.emoji-1F554 { background-position: -660px -460px; }
+.emoji-1F555 { background-position: -660px -480px; }
+.emoji-1F556 { background-position: -660px -500px; }
+.emoji-1F557 { background-position: -660px -520px; }
+.emoji-1F558 { background-position: -660px -540px; }
+.emoji-1F559 { background-position: -660px -560px; }
+.emoji-1F55A { background-position: -660px -580px; }
+.emoji-1F55B { background-position: -660px -600px; }
+.emoji-1F55C { background-position: -660px -620px; }
+.emoji-1F55D { background-position: -660px -640px; }
+.emoji-1F55E { background-position: 0 -660px; }
+.emoji-1F55F { background-position: -20px -660px; }
+.emoji-1F560 { background-position: -40px -660px; }
+.emoji-1F561 { background-position: -60px -660px; }
+.emoji-1F562 { background-position: -80px -660px; }
+.emoji-1F563 { background-position: -100px -660px; }
+.emoji-1F564 { background-position: -120px -660px; }
+.emoji-1F565 { background-position: -140px -660px; }
+.emoji-1F566 { background-position: -160px -660px; }
+.emoji-1F567 { background-position: -180px -660px; }
+.emoji-1F568 { background-position: -200px -660px; }
+.emoji-1F569 { background-position: -220px -660px; }
+.emoji-1F56A { background-position: -240px -660px; }
+.emoji-1F56B { background-position: -260px -660px; }
+.emoji-1F56C { background-position: -280px -660px; }
+.emoji-1F56D { background-position: -300px -660px; }
+.emoji-1F56E { background-position: -320px -660px; }
+.emoji-1F56F { background-position: -340px -660px; }
+.emoji-1F570 { background-position: -360px -660px; }
+.emoji-1F571 { background-position: -380px -660px; }
+.emoji-1F572 { background-position: -400px -660px; }
+.emoji-1F573 { background-position: -420px -660px; }
+.emoji-1F574 { background-position: -440px -660px; }
+.emoji-1F575 { background-position: -460px -660px; }
+.emoji-1F575-1F3FB { background-position: -480px -660px; }
+.emoji-1F575-1F3FC { background-position: -500px -660px; }
+.emoji-1F575-1F3FD { background-position: -520px -660px; }
+.emoji-1F575-1F3FE { background-position: -540px -660px; }
+.emoji-1F575-1F3FF { background-position: -560px -660px; }
+.emoji-1F576 { background-position: -580px -660px; }
+.emoji-1F577 { background-position: -600px -660px; }
+.emoji-1F578 { background-position: -620px -660px; }
+.emoji-1F579 { background-position: -640px -660px; }
+.emoji-1F57B { background-position: -660px -660px; }
+.emoji-1F57E { background-position: -680px 0; }
+.emoji-1F57F { background-position: -680px -20px; }
+.emoji-1F581 { background-position: -680px -40px; }
+.emoji-1F582 { background-position: -680px -60px; }
+.emoji-1F583 { background-position: -680px -80px; }
+.emoji-1F585 { background-position: -680px -100px; }
+.emoji-1F586 { background-position: -680px -120px; }
+.emoji-1F587 { background-position: -680px -140px; }
+.emoji-1F588 { background-position: -680px -160px; }
+.emoji-1F589 { background-position: -680px -180px; }
+.emoji-1F58A { background-position: -680px -200px; }
+.emoji-1F58B { background-position: -680px -220px; }
+.emoji-1F58C { background-position: -680px -240px; }
+.emoji-1F58D { background-position: -680px -260px; }
+.emoji-1F58E { background-position: -680px -280px; }
+.emoji-1F58F { background-position: -680px -300px; }
+.emoji-1F590 { background-position: -680px -320px; }
+.emoji-1F590-1F3FB { background-position: -680px -340px; }
+.emoji-1F590-1F3FC { background-position: -680px -360px; }
+.emoji-1F590-1F3FD { background-position: -680px -380px; }
+.emoji-1F590-1F3FE { background-position: -680px -400px; }
+.emoji-1F590-1F3FF { background-position: -680px -420px; }
+.emoji-1F591 { background-position: -680px -440px; }
+.emoji-1F592 { background-position: -680px -460px; }
+.emoji-1F593 { background-position: -680px -480px; }
+.emoji-1F594 { background-position: -680px -500px; }
+.emoji-1F595 { background-position: -680px -520px; }
+.emoji-1F595-1F3FB { background-position: -680px -540px; }
+.emoji-1F595-1F3FC { background-position: -680px -560px; }
+.emoji-1F595-1F3FD { background-position: -680px -580px; }
+.emoji-1F595-1F3FE { background-position: -680px -600px; }
+.emoji-1F595-1F3FF { background-position: -680px -620px; }
+.emoji-1F596 { background-position: -680px -640px; }
+.emoji-1F596-1F3FB { background-position: -680px -660px; }
+.emoji-1F596-1F3FC { background-position: 0 -680px; }
+.emoji-1F596-1F3FD { background-position: -20px -680px; }
+.emoji-1F596-1F3FE { background-position: -40px -680px; }
+.emoji-1F596-1F3FF { background-position: -60px -680px; }
+.emoji-1F597 { background-position: -80px -680px; }
+.emoji-1F598 { background-position: -100px -680px; }
+.emoji-1F599 { background-position: -120px -680px; }
+.emoji-1F59E { background-position: -140px -680px; }
+.emoji-1F59F { background-position: -160px -680px; }
+.emoji-1F5A5 { background-position: -180px -680px; }
+.emoji-1F5A6 { background-position: -200px -680px; }
+.emoji-1F5A7 { background-position: -220px -680px; }
+.emoji-1F5A8 { background-position: -240px -680px; }
+.emoji-1F5A9 { background-position: -260px -680px; }
+.emoji-1F5AA { background-position: -280px -680px; }
+.emoji-1F5AB { background-position: -300px -680px; }
+.emoji-1F5AD { background-position: -320px -680px; }
+.emoji-1F5AE { background-position: -340px -680px; }
+.emoji-1F5AF { background-position: -360px -680px; }
+.emoji-1F5B1 { background-position: -380px -680px; }
+.emoji-1F5B2 { background-position: -400px -680px; }
+.emoji-1F5B3 { background-position: -420px -680px; }
+.emoji-1F5B4 { background-position: -440px -680px; }
+.emoji-1F5B8 { background-position: -460px -680px; }
+.emoji-1F5B9 { background-position: -480px -680px; }
+.emoji-1F5BC { background-position: -500px -680px; }
+.emoji-1F5BD { background-position: -520px -680px; }
+.emoji-1F5BE { background-position: -540px -680px; }
+.emoji-1F5C0 { background-position: -560px -680px; }
+.emoji-1F5C1 { background-position: -580px -680px; }
+.emoji-1F5C2 { background-position: -600px -680px; }
+.emoji-1F5C3 { background-position: -620px -680px; }
+.emoji-1F5C4 { background-position: -640px -680px; }
+.emoji-1F5C6 { background-position: -660px -680px; }
+.emoji-1F5C7 { background-position: -680px -680px; }
+.emoji-1F5C9 { background-position: -700px 0; }
+.emoji-1F5CA { background-position: -700px -20px; }
+.emoji-1F5CE { background-position: -700px -40px; }
+.emoji-1F5CF { background-position: -700px -60px; }
+.emoji-1F5D0 { background-position: -700px -80px; }
+.emoji-1F5D1 { background-position: -700px -100px; }
+.emoji-1F5D2 { background-position: -700px -120px; }
+.emoji-1F5D3 { background-position: -700px -140px; }
+.emoji-1F5D4 { background-position: -700px -160px; }
+.emoji-1F5D8 { background-position: -700px -180px; }
+.emoji-1F5D9 { background-position: -700px -200px; }
+.emoji-1F5DC { background-position: -700px -220px; }
+.emoji-1F5DD { background-position: -700px -240px; }
+.emoji-1F5DE { background-position: -700px -260px; }
+.emoji-1F5E0 { background-position: -700px -280px; }
+.emoji-1F5E1 { background-position: -700px -300px; }
+.emoji-1F5E2 { background-position: -700px -320px; }
+.emoji-1F5E3 { background-position: -700px -340px; }
+.emoji-1F5E8 { background-position: -700px -360px; }
+.emoji-1F5E9 { background-position: -700px -380px; }
+.emoji-1F5EA { background-position: -700px -400px; }
+.emoji-1F5EB { background-position: -700px -420px; }
+.emoji-1F5EC { background-position: -700px -440px; }
+.emoji-1F5ED { background-position: -700px -460px; }
+.emoji-1F5EE { background-position: -700px -480px; }
+.emoji-1F5EF { background-position: -700px -500px; }
+.emoji-1F5F0 { background-position: -700px -520px; }
+.emoji-1F5F1 { background-position: -700px -540px; }
+.emoji-1F5F2 { background-position: -700px -560px; }
+.emoji-1F5F3 { background-position: -700px -580px; }
+.emoji-1F5F4 { background-position: -700px -600px; }
+.emoji-1F5F5 { background-position: -700px -620px; }
+.emoji-1F5F8 { background-position: -700px -640px; }
+.emoji-1F5F9 { background-position: -700px -660px; }
+.emoji-1F5FA { background-position: -700px -680px; }
+.emoji-1F5FB { background-position: 0 -700px; }
+.emoji-1F5FC { background-position: -20px -700px; }
+.emoji-1F5FD { background-position: -40px -700px; }
+.emoji-1F5FE { background-position: -60px -700px; }
+.emoji-1F5FF { background-position: -80px -700px; }
+.emoji-1F600 { background-position: -100px -700px; }
+.emoji-1F601 { background-position: -120px -700px; }
+.emoji-1F602 { background-position: -140px -700px; }
+.emoji-1F603 { background-position: -160px -700px; }
+.emoji-1F604 { background-position: -180px -700px; }
+.emoji-1F605 { background-position: -200px -700px; }
+.emoji-1F606 { background-position: -220px -700px; }
+.emoji-1F607 { background-position: -240px -700px; }
+.emoji-1F608 { background-position: -260px -700px; }
+.emoji-1F609 { background-position: -280px -700px; }
+.emoji-1F60A { background-position: -300px -700px; }
+.emoji-1F60B { background-position: -320px -700px; }
+.emoji-1F60C { background-position: -340px -700px; }
+.emoji-1F60D { background-position: -360px -700px; }
+.emoji-1F60E { background-position: -380px -700px; }
+.emoji-1F60F { background-position: -400px -700px; }
+.emoji-1F610 { background-position: -420px -700px; }
+.emoji-1F611 { background-position: -440px -700px; }
+.emoji-1F612 { background-position: -460px -700px; }
+.emoji-1F613 { background-position: -480px -700px; }
+.emoji-1F614 { background-position: -500px -700px; }
+.emoji-1F615 { background-position: -520px -700px; }
+.emoji-1F616 { background-position: -540px -700px; }
+.emoji-1F617 { background-position: -560px -700px; }
+.emoji-1F618 { background-position: -580px -700px; }
+.emoji-1F619 { background-position: -600px -700px; }
+.emoji-1F61A { background-position: -620px -700px; }
+.emoji-1F61B { background-position: -640px -700px; }
+.emoji-1F61C { background-position: -660px -700px; }
+.emoji-1F61D { background-position: -680px -700px; }
+.emoji-1F61E { background-position: -700px -700px; }
+.emoji-1F61F { background-position: -720px 0; }
+.emoji-1F620 { background-position: -720px -20px; }
+.emoji-1F621 { background-position: -720px -40px; }
+.emoji-1F622 { background-position: -720px -60px; }
+.emoji-1F623 { background-position: -720px -80px; }
+.emoji-1F624 { background-position: -720px -100px; }
+.emoji-1F625 { background-position: -720px -120px; }
+.emoji-1F626 { background-position: -720px -140px; }
+.emoji-1F627 { background-position: -720px -160px; }
+.emoji-1F628 { background-position: -720px -180px; }
+.emoji-1F629 { background-position: -720px -200px; }
+.emoji-1F62A { background-position: -720px -220px; }
+.emoji-1F62B { background-position: -720px -240px; }
+.emoji-1F62C { background-position: -720px -260px; }
+.emoji-1F62D { background-position: -720px -280px; }
+.emoji-1F62E { background-position: -720px -300px; }
+.emoji-1F62F { background-position: -720px -320px; }
+.emoji-1F630 { background-position: -720px -340px; }
+.emoji-1F631 { background-position: -720px -360px; }
+.emoji-1F632 { background-position: -720px -380px; }
+.emoji-1F633 { background-position: -720px -400px; }
+.emoji-1F634 { background-position: -720px -420px; }
+.emoji-1F635 { background-position: -720px -440px; }
+.emoji-1F636 { background-position: -720px -460px; }
+.emoji-1F637 { background-position: -720px -480px; }
+.emoji-1F638 { background-position: -720px -500px; }
+.emoji-1F639 { background-position: -720px -520px; }
+.emoji-1F63A { background-position: -720px -540px; }
+.emoji-1F63B { background-position: -720px -560px; }
+.emoji-1F63C { background-position: -720px -580px; }
+.emoji-1F63D { background-position: -720px -600px; }
+.emoji-1F63E { background-position: -720px -620px; }
+.emoji-1F63F { background-position: -720px -640px; }
+.emoji-1F640 { background-position: -720px -660px; }
+.emoji-1F641 { background-position: -720px -680px; }
+.emoji-1F642 { background-position: -720px -700px; }
+.emoji-1F643 { background-position: 0 -720px; }
+.emoji-1F644 { background-position: -20px -720px; }
+.emoji-1F645 { background-position: -40px -720px; }
+.emoji-1F645-1F3FB { background-position: -60px -720px; }
+.emoji-1F645-1F3FC { background-position: -80px -720px; }
+.emoji-1F645-1F3FD { background-position: -100px -720px; }
+.emoji-1F645-1F3FE { background-position: -120px -720px; }
+.emoji-1F645-1F3FF { background-position: -140px -720px; }
+.emoji-1F646 { background-position: -160px -720px; }
+.emoji-1F646-1F3FB { background-position: -180px -720px; }
+.emoji-1F646-1F3FC { background-position: -200px -720px; }
+.emoji-1F646-1F3FD { background-position: -220px -720px; }
+.emoji-1F646-1F3FE { background-position: -240px -720px; }
+.emoji-1F646-1F3FF { background-position: -260px -720px; }
+.emoji-1F647 { background-position: -280px -720px; }
+.emoji-1F647-1F3FB { background-position: -300px -720px; }
+.emoji-1F647-1F3FC { background-position: -320px -720px; }
+.emoji-1F647-1F3FD { background-position: -340px -720px; }
+.emoji-1F647-1F3FE { background-position: -360px -720px; }
+.emoji-1F647-1F3FF { background-position: -380px -720px; }
+.emoji-1F648 { background-position: -400px -720px; }
+.emoji-1F649 { background-position: -420px -720px; }
+.emoji-1F64A { background-position: -440px -720px; }
+.emoji-1F64B { background-position: -460px -720px; }
+.emoji-1F64B-1F3FB { background-position: -480px -720px; }
+.emoji-1F64B-1F3FC { background-position: -500px -720px; }
+.emoji-1F64B-1F3FD { background-position: -520px -720px; }
+.emoji-1F64B-1F3FE { background-position: -540px -720px; }
+.emoji-1F64B-1F3FF { background-position: -560px -720px; }
+.emoji-1F64C { background-position: -580px -720px; }
+.emoji-1F64C-1F3FB { background-position: -600px -720px; }
+.emoji-1F64C-1F3FC { background-position: -620px -720px; }
+.emoji-1F64C-1F3FD { background-position: -640px -720px; }
+.emoji-1F64C-1F3FE { background-position: -660px -720px; }
+.emoji-1F64C-1F3FF { background-position: -680px -720px; }
+.emoji-1F64D { background-position: -700px -720px; }
+.emoji-1F64D-1F3FB { background-position: -720px -720px; }
+.emoji-1F64D-1F3FC { background-position: -740px 0; }
+.emoji-1F64D-1F3FD { background-position: -740px -20px; }
+.emoji-1F64D-1F3FE { background-position: -740px -40px; }
+.emoji-1F64D-1F3FF { background-position: -740px -60px; }
+.emoji-1F64E { background-position: -740px -80px; }
+.emoji-1F64E-1F3FB { background-position: -740px -100px; }
+.emoji-1F64E-1F3FC { background-position: -740px -120px; }
+.emoji-1F64E-1F3FD { background-position: -740px -140px; }
+.emoji-1F64E-1F3FE { background-position: -740px -160px; }
+.emoji-1F64E-1F3FF { background-position: -740px -180px; }
+.emoji-1F64F { background-position: -740px -200px; }
+.emoji-1F64F-1F3FB { background-position: -740px -220px; }
+.emoji-1F64F-1F3FC { background-position: -740px -240px; }
+.emoji-1F64F-1F3FD { background-position: -740px -260px; }
+.emoji-1F64F-1F3FE { background-position: -740px -280px; }
+.emoji-1F64F-1F3FF { background-position: -740px -300px; }
+.emoji-1F680 { background-position: -740px -320px; }
+.emoji-1F681 { background-position: -740px -340px; }
+.emoji-1F682 { background-position: -740px -360px; }
+.emoji-1F683 { background-position: -740px -380px; }
+.emoji-1F684 { background-position: -740px -400px; }
+.emoji-1F685 { background-position: -740px -420px; }
+.emoji-1F686 { background-position: -740px -440px; }
+.emoji-1F687 { background-position: -740px -460px; }
+.emoji-1F688 { background-position: -740px -480px; }
+.emoji-1F689 { background-position: -740px -500px; }
+.emoji-1F68A { background-position: -740px -520px; }
+.emoji-1F68B { background-position: -740px -540px; }
+.emoji-1F68C { background-position: -740px -560px; }
+.emoji-1F68D { background-position: -740px -580px; }
+.emoji-1F68E { background-position: -740px -600px; }
+.emoji-1F68F { background-position: -740px -620px; }
+.emoji-1F690 { background-position: -740px -640px; }
+.emoji-1F691 { background-position: -740px -660px; }
+.emoji-1F692 { background-position: -740px -680px; }
+.emoji-1F693 { background-position: -740px -700px; }
+.emoji-1F694 { background-position: -740px -720px; }
+.emoji-1F695 { background-position: 0 -740px; }
+.emoji-1F696 { background-position: -20px -740px; }
+.emoji-1F697 { background-position: -40px -740px; }
+.emoji-1F698 { background-position: -60px -740px; }
+.emoji-1F699 { background-position: -80px -740px; }
+.emoji-1F69A { background-position: -100px -740px; }
+.emoji-1F69B { background-position: -120px -740px; }
+.emoji-1F69C { background-position: -140px -740px; }
+.emoji-1F69D { background-position: -160px -740px; }
+.emoji-1F69E { background-position: -180px -740px; }
+.emoji-1F69F { background-position: -200px -740px; }
+.emoji-1F6A0 { background-position: -220px -740px; }
+.emoji-1F6A1 { background-position: -240px -740px; }
+.emoji-1F6A2 { background-position: -260px -740px; }
+.emoji-1F6A3 { background-position: -280px -740px; }
+.emoji-1F6A3-1F3FB { background-position: -300px -740px; }
+.emoji-1F6A3-1F3FC { background-position: -320px -740px; }
+.emoji-1F6A3-1F3FD { background-position: -340px -740px; }
+.emoji-1F6A3-1F3FE { background-position: -360px -740px; }
+.emoji-1F6A3-1F3FF { background-position: -380px -740px; }
+.emoji-1F6A4 { background-position: -400px -740px; }
+.emoji-1F6A5 { background-position: -420px -740px; }
+.emoji-1F6A6 { background-position: -440px -740px; }
+.emoji-1F6A7 { background-position: -460px -740px; }
+.emoji-1F6A8 { background-position: -480px -740px; }
+.emoji-1F6A9 { background-position: -500px -740px; }
+.emoji-1F6AA { background-position: -520px -740px; }
+.emoji-1F6AB { background-position: -540px -740px; }
+.emoji-1F6AC { background-position: -560px -740px; }
+.emoji-1F6AD { background-position: -580px -740px; }
+.emoji-1F6AE { background-position: -600px -740px; }
+.emoji-1F6AF { background-position: -620px -740px; }
+.emoji-1F6B0 { background-position: -640px -740px; }
+.emoji-1F6B1 { background-position: -660px -740px; }
+.emoji-1F6B2 { background-position: -680px -740px; }
+.emoji-1F6B3 { background-position: -700px -740px; }
+.emoji-1F6B4 { background-position: -720px -740px; }
+.emoji-1F6B4-1F3FB { background-position: -740px -740px; }
+.emoji-1F6B4-1F3FC { background-position: -760px 0; }
+.emoji-1F6B4-1F3FD { background-position: -760px -20px; }
+.emoji-1F6B4-1F3FE { background-position: -760px -40px; }
+.emoji-1F6B4-1F3FF { background-position: -760px -60px; }
+.emoji-1F6B5 { background-position: -760px -80px; }
+.emoji-1F6B5-1F3FB { background-position: -760px -100px; }
+.emoji-1F6B5-1F3FC { background-position: -760px -120px; }
+.emoji-1F6B5-1F3FD { background-position: -760px -140px; }
+.emoji-1F6B5-1F3FE { background-position: -760px -160px; }
+.emoji-1F6B5-1F3FF { background-position: -760px -180px; }
+.emoji-1F6B6 { background-position: -760px -200px; }
+.emoji-1F6B6-1F3FB { background-position: -760px -220px; }
+.emoji-1F6B6-1F3FC { background-position: -760px -240px; }
+.emoji-1F6B6-1F3FD { background-position: -760px -260px; }
+.emoji-1F6B6-1F3FE { background-position: -760px -280px; }
+.emoji-1F6B6-1F3FF { background-position: -760px -300px; }
+.emoji-1F6B7 { background-position: -760px -320px; }
+.emoji-1F6B8 { background-position: -760px -340px; }
+.emoji-1F6B9 { background-position: -760px -360px; }
+.emoji-1F6BA { background-position: -760px -380px; }
+.emoji-1F6BB { background-position: -760px -400px; }
+.emoji-1F6BC { background-position: -760px -420px; }
+.emoji-1F6BD { background-position: -760px -440px; }
+.emoji-1F6BE { background-position: -760px -460px; }
+.emoji-1F6BF { background-position: -760px -480px; }
+.emoji-1F6C0 { background-position: -760px -500px; }
+.emoji-1F6C0-1F3FB { background-position: -760px -520px; }
+.emoji-1F6C0-1F3FC { background-position: -760px -540px; }
+.emoji-1F6C0-1F3FD { background-position: -760px -560px; }
+.emoji-1F6C0-1F3FE { background-position: -760px -580px; }
+.emoji-1F6C0-1F3FF { background-position: -760px -600px; }
+.emoji-1F6C1 { background-position: -760px -620px; }
+.emoji-1F6C2 { background-position: -760px -640px; }
+.emoji-1F6C3 { background-position: -760px -660px; }
+.emoji-1F6C4 { background-position: -760px -680px; }
+.emoji-1F6C5 { background-position: -760px -700px; }
+.emoji-1F6C6 { background-position: -760px -720px; }
+.emoji-1F6C7 { background-position: -760px -740px; }
+.emoji-1F6C8 { background-position: 0 -760px; }
+.emoji-1F6C9 { background-position: -20px -760px; }
+.emoji-1F6CA { background-position: -40px -760px; }
+.emoji-1F6CB { background-position: -60px -760px; }
+.emoji-1F6CC { background-position: -80px -760px; }
+.emoji-1F6CD { background-position: -100px -760px; }
+.emoji-1F6CE { background-position: -120px -760px; }
+.emoji-1F6CF { background-position: -140px -760px; }
+.emoji-1F6D0 { background-position: -160px -760px; }
+.emoji-1F6E0 { background-position: -180px -760px; }
+.emoji-1F6E1 { background-position: -200px -760px; }
+.emoji-1F6E2 { background-position: -220px -760px; }
+.emoji-1F6E3 { background-position: -240px -760px; }
+.emoji-1F6E4 { background-position: -260px -760px; }
+.emoji-1F6E5 { background-position: -280px -760px; }
+.emoji-1F6E6 { background-position: -300px -760px; }
+.emoji-1F6E7 { background-position: -320px -760px; }
+.emoji-1F6E8 { background-position: -340px -760px; }
+.emoji-1F6E9 { background-position: -360px -760px; }
+.emoji-1F6EA { background-position: -380px -760px; }
+.emoji-1F6EB { background-position: -400px -760px; }
+.emoji-1F6EC { background-position: -420px -760px; }
+.emoji-1F6F0 { background-position: -440px -760px; }
+.emoji-1F6F1 { background-position: -460px -760px; }
+.emoji-1F6F2 { background-position: -480px -760px; }
+.emoji-1F6F3 { background-position: -500px -760px; }
+.emoji-1F910 { background-position: -520px -760px; }
+.emoji-1F911 { background-position: -540px -760px; }
+.emoji-1F912 { background-position: -560px -760px; }
+.emoji-1F913 { background-position: -580px -760px; }
+.emoji-1F914 { background-position: -600px -760px; }
+.emoji-1F915 { background-position: -620px -760px; }
+.emoji-1F916 { background-position: -640px -760px; }
+.emoji-1F917 { background-position: -660px -760px; }
+.emoji-1F918 { background-position: -680px -760px; }
+.emoji-1F918-1F3FB { background-position: -700px -760px; }
+.emoji-1F918-1F3FC { background-position: -720px -760px; }
+.emoji-1F918-1F3FD { background-position: -740px -760px; }
+.emoji-1F918-1F3FE { background-position: -760px -760px; }
+.emoji-1F918-1F3FF { background-position: -780px 0; }
+.emoji-1F980 { background-position: -780px -20px; }
+.emoji-1F981 { background-position: -780px -40px; }
+.emoji-1F982 { background-position: -780px -60px; }
+.emoji-1F983 { background-position: -780px -80px; }
+.emoji-1F984 { background-position: -780px -100px; }
+.emoji-1F9C0 { background-position: -780px -120px; }
+.emoji-203C { background-position: -780px -140px; }
+.emoji-2049 { background-position: -780px -160px; }
+.emoji-2122 { background-position: -780px -180px; }
+.emoji-2139 { background-position: -780px -200px; }
+.emoji-2194 { background-position: -780px -220px; }
+.emoji-2195 { background-position: -780px -240px; }
+.emoji-2196 { background-position: -780px -260px; }
+.emoji-2197 { background-position: -780px -280px; }
+.emoji-2198 { background-position: -780px -300px; }
+.emoji-2199 { background-position: -780px -320px; }
+.emoji-21A9 { background-position: -780px -340px; }
+.emoji-21AA { background-position: -780px -360px; }
+.emoji-231A { background-position: -780px -380px; }
+.emoji-231B { background-position: -780px -400px; }
+.emoji-2328 { background-position: -780px -420px; }
+.emoji-23E9 { background-position: -780px -440px; }
+.emoji-23EA { background-position: -780px -460px; }
+.emoji-23EB { background-position: -780px -480px; }
+.emoji-23EC { background-position: -780px -500px; }
+.emoji-23ED { background-position: -780px -520px; }
+.emoji-23EE { background-position: -780px -540px; }
+.emoji-23EF { background-position: -780px -560px; }
+.emoji-23F0 { background-position: -780px -580px; }
+.emoji-23F1 { background-position: -780px -600px; }
+.emoji-23F2 { background-position: -780px -620px; }
+.emoji-23F3 { background-position: -780px -640px; }
+.emoji-23F8 { background-position: -780px -660px; }
+.emoji-23F9 { background-position: -780px -680px; }
+.emoji-23FA { background-position: -780px -700px; }
+.emoji-24C2 { background-position: -780px -720px; }
+.emoji-25AA { background-position: -780px -740px; }
+.emoji-25AB { background-position: -780px -760px; }
+.emoji-25B6 { background-position: 0 -780px; }
+.emoji-25C0 { background-position: -20px -780px; }
+.emoji-25FB { background-position: -40px -780px; }
+.emoji-25FC { background-position: -60px -780px; }
+.emoji-25FD { background-position: -80px -780px; }
+.emoji-25FE { background-position: -100px -780px; }
+.emoji-2600 { background-position: -120px -780px; }
+.emoji-2601 { background-position: -140px -780px; }
+.emoji-2602 { background-position: -160px -780px; }
+.emoji-2603 { background-position: -180px -780px; }
+.emoji-2604 { background-position: -200px -780px; }
+.emoji-260E { background-position: -220px -780px; }
+.emoji-2611 { background-position: -240px -780px; }
+.emoji-2614 { background-position: -260px -780px; }
+.emoji-2615 { background-position: -280px -780px; }
+.emoji-2618 { background-position: -300px -780px; }
+.emoji-261D { background-position: -320px -780px; }
+.emoji-261D-1F3FB { background-position: -340px -780px; }
+.emoji-261D-1F3FC { background-position: -360px -780px; }
+.emoji-261D-1F3FD { background-position: -380px -780px; }
+.emoji-261D-1F3FE { background-position: -400px -780px; }
+.emoji-261D-1F3FF { background-position: -420px -780px; }
+.emoji-2620 { background-position: -440px -780px; }
+.emoji-2622 { background-position: -460px -780px; }
+.emoji-2623 { background-position: -480px -780px; }
+.emoji-2626 { background-position: -500px -780px; }
+.emoji-262A { background-position: -520px -780px; }
+.emoji-262E { background-position: -540px -780px; }
+.emoji-262F { background-position: -560px -780px; }
+.emoji-2638 { background-position: -580px -780px; }
+.emoji-2639 { background-position: -600px -780px; }
+.emoji-263A { background-position: -620px -780px; }
+.emoji-2648 { background-position: -640px -780px; }
+.emoji-2649 { background-position: -660px -780px; }
+.emoji-264A { background-position: -680px -780px; }
+.emoji-264B { background-position: -700px -780px; }
+.emoji-264C { background-position: -720px -780px; }
+.emoji-264D { background-position: -740px -780px; }
+.emoji-264E { background-position: -760px -780px; }
+.emoji-264F { background-position: -780px -780px; }
+.emoji-2650 { background-position: -800px 0; }
+.emoji-2651 { background-position: -800px -20px; }
+.emoji-2652 { background-position: -800px -40px; }
+.emoji-2653 { background-position: -800px -60px; }
+.emoji-2660 { background-position: -800px -80px; }
+.emoji-2663 { background-position: -800px -100px; }
+.emoji-2665 { background-position: -800px -120px; }
+.emoji-2666 { background-position: -800px -140px; }
+.emoji-2668 { background-position: -800px -160px; }
+.emoji-267B { background-position: -800px -180px; }
+.emoji-267F { background-position: -800px -200px; }
+.emoji-2692 { background-position: -800px -220px; }
+.emoji-2693 { background-position: -800px -240px; }
+.emoji-2694 { background-position: -800px -260px; }
+.emoji-2696 { background-position: -800px -280px; }
+.emoji-2697 { background-position: -800px -300px; }
+.emoji-2699 { background-position: -800px -320px; }
+.emoji-269B { background-position: -800px -340px; }
+.emoji-269C { background-position: -800px -360px; }
+.emoji-26A0 { background-position: -800px -380px; }
+.emoji-26A1 { background-position: -800px -400px; }
+.emoji-26AA { background-position: -800px -420px; }
+.emoji-26AB { background-position: -800px -440px; }
+.emoji-26B0 { background-position: -800px -460px; }
+.emoji-26B1 { background-position: -800px -480px; }
+.emoji-26BD { background-position: -800px -500px; }
+.emoji-26BE { background-position: -800px -520px; }
+.emoji-26C4 { background-position: -800px -540px; }
+.emoji-26C5 { background-position: -800px -560px; }
+.emoji-26C8 { background-position: -800px -580px; }
+.emoji-26CE { background-position: -800px -600px; }
+.emoji-26CF { background-position: -800px -620px; }
+.emoji-26D1 { background-position: -800px -640px; }
+.emoji-26D3 { background-position: -800px -660px; }
+.emoji-26D4 { background-position: -800px -680px; }
+.emoji-26E9 { background-position: -800px -700px; }
+.emoji-26EA { background-position: -800px -720px; }
+.emoji-26F0 { background-position: -800px -740px; }
+.emoji-26F1 { background-position: -800px -760px; }
+.emoji-26F2 { background-position: -800px -780px; }
+.emoji-26F3 { background-position: 0 -800px; }
+.emoji-26F4 { background-position: -20px -800px; }
+.emoji-26F5 { background-position: -40px -800px; }
+.emoji-26F7 { background-position: -60px -800px; }
+.emoji-26F8 { background-position: -80px -800px; }
+.emoji-26F9 { background-position: -100px -800px; }
+.emoji-26F9-1F3FB { background-position: -120px -800px; }
+.emoji-26F9-1F3FC { background-position: -140px -800px; }
+.emoji-26F9-1F3FD { background-position: -160px -800px; }
+.emoji-26F9-1F3FE { background-position: -180px -800px; }
+.emoji-26F9-1F3FF { background-position: -200px -800px; }
+.emoji-26FA { background-position: -220px -800px; }
+.emoji-26FD { background-position: -240px -800px; }
+.emoji-2702 { background-position: -260px -800px; }
+.emoji-2705 { background-position: -280px -800px; }
+.emoji-2708 { background-position: -300px -800px; }
+.emoji-2709 { background-position: -320px -800px; }
+.emoji-270A { background-position: -340px -800px; }
+.emoji-270A-1F3FB { background-position: -360px -800px; }
+.emoji-270A-1F3FC { background-position: -380px -800px; }
+.emoji-270A-1F3FD { background-position: -400px -800px; }
+.emoji-270A-1F3FE { background-position: -420px -800px; }
+.emoji-270A-1F3FF { background-position: -440px -800px; }
+.emoji-270B { background-position: -460px -800px; }
+.emoji-270B-1F3FB { background-position: -480px -800px; }
+.emoji-270B-1F3FC { background-position: -500px -800px; }
+.emoji-270B-1F3FD { background-position: -520px -800px; }
+.emoji-270B-1F3FE { background-position: -540px -800px; }
+.emoji-270B-1F3FF { background-position: -560px -800px; }
+.emoji-270C { background-position: -580px -800px; }
+.emoji-270C-1F3FB { background-position: -600px -800px; }
+.emoji-270C-1F3FC { background-position: -620px -800px; }
+.emoji-270C-1F3FD { background-position: -640px -800px; }
+.emoji-270C-1F3FE { background-position: -660px -800px; }
+.emoji-270C-1F3FF { background-position: -680px -800px; }
+.emoji-270D { background-position: -700px -800px; }
+.emoji-270D-1F3FB { background-position: -720px -800px; }
+.emoji-270D-1F3FC { background-position: -740px -800px; }
+.emoji-270D-1F3FD { background-position: -760px -800px; }
+.emoji-270D-1F3FE { background-position: -780px -800px; }
+.emoji-270D-1F3FF { background-position: -800px -800px; }
+.emoji-270F { background-position: -820px 0; }
+.emoji-2712 { background-position: -820px -20px; }
+.emoji-2714 { background-position: -820px -40px; }
+.emoji-2716 { background-position: -820px -60px; }
+.emoji-271D { background-position: -820px -80px; }
+.emoji-2721 { background-position: -820px -100px; }
+.emoji-2728 { background-position: -820px -120px; }
+.emoji-2733 { background-position: -820px -140px; }
+.emoji-2734 { background-position: -820px -160px; }
+.emoji-2744 { background-position: -820px -180px; }
+.emoji-2747 { background-position: -820px -200px; }
+.emoji-274C { background-position: -820px -220px; }
+.emoji-274E { background-position: -820px -240px; }
+.emoji-2753 { background-position: -820px -260px; }
+.emoji-2754 { background-position: -820px -280px; }
+.emoji-2755 { background-position: -820px -300px; }
+.emoji-2757 { background-position: -820px -320px; }
+.emoji-2763 { background-position: -820px -340px; }
+.emoji-2764 { background-position: -820px -360px; }
+.emoji-2795 { background-position: -820px -380px; }
+.emoji-2796 { background-position: -820px -400px; }
+.emoji-2797 { background-position: -820px -420px; }
+.emoji-27A1 { background-position: -820px -440px; }
+.emoji-27B0 { background-position: -820px -460px; }
+.emoji-27BF { background-position: -820px -480px; }
+.emoji-2934 { background-position: -820px -500px; }
+.emoji-2935 { background-position: -820px -520px; }
+.emoji-2B05 { background-position: -820px -540px; }
+.emoji-2B06 { background-position: -820px -560px; }
+.emoji-2B07 { background-position: -820px -580px; }
+.emoji-2B1B { background-position: -820px -600px; }
+.emoji-2B1C { background-position: -820px -620px; }
+.emoji-2B50 { background-position: -820px -640px; }
+.emoji-2B55 { background-position: -820px -660px; }
+.emoji-3030 { background-position: -820px -680px; }
+.emoji-303D { background-position: -820px -700px; }
+.emoji-3297 { background-position: -820px -720px; }
+.emoji-3299 { background-position: -820px -740px; }
-.emoji-icon{
- background-image: image-url("emoji.png");
+.emoji-icon {
+ background-image: image-url('emoji.png');
background-repeat: no-repeat;
-}
+ height: 20px;
+ width: 20px;
-.emoji-0023-20E3 { background-position: 0px 0px; }
-.emoji-0030-20E3 { background-position: -20px 0px; }
-.emoji-0031-20E3 { background-position: -40px 0px; }
-.emoji-0032-20E3 { background-position: -60px 0px; }
-.emoji-0033-20E3 { background-position: -80px 0px; }
-.emoji-0034-20E3 { background-position: -100px 0px; }
-.emoji-0035-20E3 { background-position: -120px 0px; }
-.emoji-0036-20E3 { background-position: -140px 0px; }
-.emoji-0037-20E3 { background-position: -160px 0px; }
-.emoji-0038-20E3 { background-position: -180px 0px; }
-.emoji-0039-20E3 { background-position: -200px 0px; }
-.emoji-00A9 { background-position: -220px 0px; }
-.emoji-00AE { background-position: -240px 0px; }
-.emoji-1F004 { background-position: -260px 0px; }
-.emoji-1F0CF { background-position: -280px 0px; }
-.emoji-1F170 { background-position: -300px 0px; }
-.emoji-1F171 { background-position: -320px 0px; }
-.emoji-1F17E { background-position: -340px 0px; }
-.emoji-1F17F { background-position: -360px 0px; }
-.emoji-1F18E { background-position: -380px 0px; }
-.emoji-1F191 { background-position: -400px 0px; }
-.emoji-1F192 { background-position: -420px 0px; }
-.emoji-1F193 { background-position: -440px 0px; }
-.emoji-1F194 { background-position: -460px 0px; }
-.emoji-1F195 { background-position: -480px 0px; }
-.emoji-1F196 { background-position: -500px 0px; }
-.emoji-1F197 { background-position: -520px 0px; }
-.emoji-1F198 { background-position: -540px 0px; }
-.emoji-1F199 { background-position: -560px 0px; }
-.emoji-1F19A { background-position: -580px 0px; }
-.emoji-1F1E6-1F1E8 { background-position: -600px 0px; }
-.emoji-1F1E6-1F1E9 { background-position: -620px 0px; }
-.emoji-1F1E6-1F1EA { background-position: -640px 0px; }
-.emoji-1F1E6-1F1EB { background-position: -660px 0px; }
-.emoji-1F1E6-1F1EC { background-position: -680px 0px; }
-.emoji-1F1E6-1F1EE { background-position: -700px 0px; }
-.emoji-1F1E6-1F1F1 { background-position: -720px 0px; }
-.emoji-1F1E6-1F1F2 { background-position: -740px 0px; }
-.emoji-1F1E6-1F1F4 { background-position: -760px 0px; }
-.emoji-1F1E6-1F1F7 { background-position: -780px 0px; }
-.emoji-1F1E6-1F1F9 { background-position: -800px 0px; }
-.emoji-1F1E6-1F1FA { background-position: -820px 0px; }
-.emoji-1F1E6-1F1FC { background-position: -840px 0px; }
-.emoji-1F1E6-1F1FF { background-position: -860px 0px; }
-.emoji-1F1E7-1F1E6 { background-position: -880px 0px; }
-.emoji-1F1E7-1F1E7 { background-position: -900px 0px; }
-.emoji-1F1E7-1F1E9 { background-position: -920px 0px; }
-.emoji-1F1E7-1F1EA { background-position: -940px 0px; }
-.emoji-1F1E7-1F1EB { background-position: -960px 0px; }
-.emoji-1F1E7-1F1EC { background-position: -980px 0px; }
-.emoji-1F1E7-1F1ED { background-position: -1000px 0px; }
-.emoji-1F1E7-1F1EE { background-position: -1020px 0px; }
-.emoji-1F1E7-1F1EF { background-position: -1040px 0px; }
-.emoji-1F1E7-1F1F2 { background-position: -1060px 0px; }
-.emoji-1F1E7-1F1F3 { background-position: -1080px 0px; }
-.emoji-1F1E7-1F1F4 { background-position: -1100px 0px; }
-.emoji-1F1E7-1F1F7 { background-position: -1120px 0px; }
-.emoji-1F1E7-1F1F8 { background-position: -1140px 0px; }
-.emoji-1F1E7-1F1F9 { background-position: -1160px 0px; }
-.emoji-1F1E7-1F1FC { background-position: -1180px 0px; }
-.emoji-1F1E7-1F1FE { background-position: -1200px 0px; }
-.emoji-1F1E7-1F1FF { background-position: -1220px 0px; }
-.emoji-1F1E8-1F1E6 { background-position: -1240px 0px; }
-.emoji-1F1E8-1F1E9 { background-position: -1260px 0px; }
-.emoji-1F1E8-1F1EB { background-position: -1280px 0px; }
-.emoji-1F1E8-1F1EC { background-position: -1300px 0px; }
-.emoji-1F1E8-1F1ED { background-position: -1320px 0px; }
-.emoji-1F1E8-1F1EE { background-position: -1340px 0px; }
-.emoji-1F1E8-1F1F1 { background-position: -1360px 0px; }
-.emoji-1F1E8-1F1F2 { background-position: -1380px 0px; }
-.emoji-1F1E8-1F1F3 { background-position: -1400px 0px; }
-.emoji-1F1E8-1F1F4 { background-position: -1420px 0px; }
-.emoji-1F1E8-1F1F7 { background-position: -1440px 0px; }
-.emoji-1F1E8-1F1FA { background-position: -1460px 0px; }
-.emoji-1F1E8-1F1FB { background-position: -1480px 0px; }
-.emoji-1F1E8-1F1FE { background-position: -1500px 0px; }
-.emoji-1F1E8-1F1FF { background-position: -1520px 0px; }
-.emoji-1F1E9-1F1EA { background-position: -1540px 0px; }
-.emoji-1F1E9-1F1EF { background-position: -1560px 0px; }
-.emoji-1F1E9-1F1F0 { background-position: -1580px 0px; }
-.emoji-1F1E9-1F1F2 { background-position: -1600px 0px; }
-.emoji-1F1E9-1F1F4 { background-position: -1620px 0px; }
-.emoji-1F1E9-1F1FF { background-position: -1640px 0px; }
-.emoji-1F1EA-1F1E8 { background-position: -1660px 0px; }
-.emoji-1F1EA-1F1EA { background-position: -1680px 0px; }
-.emoji-1F1EA-1F1EC { background-position: -1700px 0px; }
-.emoji-1F1EA-1F1ED { background-position: -1720px 0px; }
-.emoji-1F1EA-1F1F7 { background-position: -1740px 0px; }
-.emoji-1F1EA-1F1F8 { background-position: -1760px 0px; }
-.emoji-1F1EA-1F1F9 { background-position: -1780px 0px; }
-.emoji-1F1EB-1F1EE { background-position: -1800px 0px; }
-.emoji-1F1EB-1F1EF { background-position: -1820px 0px; }
-.emoji-1F1EB-1F1F0 { background-position: -1840px 0px; }
-.emoji-1F1EB-1F1F2 { background-position: -1860px 0px; }
-.emoji-1F1EB-1F1F4 { background-position: -1880px 0px; }
-.emoji-1F1EB-1F1F7 { background-position: -1900px 0px; }
-.emoji-1F1EC-1F1E6 { background-position: -1920px 0px; }
-.emoji-1F1EC-1F1E7 { background-position: -1940px 0px; }
-.emoji-1F1EC-1F1E9 { background-position: -1960px 0px; }
-.emoji-1F1EC-1F1EA { background-position: -1980px 0px; }
-.emoji-1F1EC-1F1ED { background-position: -2000px 0px; }
-.emoji-1F1EC-1F1EE { background-position: -2020px 0px; }
-.emoji-1F1EC-1F1F1 { background-position: -2040px 0px; }
-.emoji-1F1EC-1F1F2 { background-position: -2060px 0px; }
-.emoji-1F1EC-1F1F3 { background-position: -2080px 0px; }
-.emoji-1F1EC-1F1F6 { background-position: -2100px 0px; }
-.emoji-1F1EC-1F1F7 { background-position: -2120px 0px; }
-.emoji-1F1EC-1F1F9 { background-position: -2140px 0px; }
-.emoji-1F1EC-1F1FA { background-position: -2160px 0px; }
-.emoji-1F1EC-1F1FC { background-position: -2180px 0px; }
-.emoji-1F1EC-1F1FE { background-position: -2200px 0px; }
-.emoji-1F1ED-1F1F0 { background-position: -2220px 0px; }
-.emoji-1F1ED-1F1F3 { background-position: -2240px 0px; }
-.emoji-1F1ED-1F1F7 { background-position: -2260px 0px; }
-.emoji-1F1ED-1F1F9 { background-position: -2280px 0px; }
-.emoji-1F1ED-1F1FA { background-position: -2300px 0px; }
-.emoji-1F1EE-1F1E9 { background-position: -2320px 0px; }
-.emoji-1F1EE-1F1EA { background-position: -2340px 0px; }
-.emoji-1F1EE-1F1F1 { background-position: -2360px 0px; }
-.emoji-1F1EE-1F1F3 { background-position: -2380px 0px; }
-.emoji-1F1EE-1F1F6 { background-position: -2400px 0px; }
-.emoji-1F1EE-1F1F7 { background-position: -2420px 0px; }
-.emoji-1F1EE-1F1F8 { background-position: -2440px 0px; }
-.emoji-1F1EE-1F1F9 { background-position: -2460px 0px; }
-.emoji-1F1EF-1F1EA { background-position: -2480px 0px; }
-.emoji-1F1EF-1F1F2 { background-position: -2500px 0px; }
-.emoji-1F1EF-1F1F4 { background-position: -2520px 0px; }
-.emoji-1F1EF-1F1F5 { background-position: -2540px 0px; }
-.emoji-1F1F0-1F1EA { background-position: -2560px 0px; }
-.emoji-1F1F0-1F1EC { background-position: -2580px 0px; }
-.emoji-1F1F0-1F1ED { background-position: -2600px 0px; }
-.emoji-1F1F0-1F1EE { background-position: -2620px 0px; }
-.emoji-1F1F0-1F1F2 { background-position: -2640px 0px; }
-.emoji-1F1F0-1F1F3 { background-position: -2660px 0px; }
-.emoji-1F1F0-1F1F5 { background-position: -2680px 0px; }
-.emoji-1F1F0-1F1F7 { background-position: -2700px 0px; }
-.emoji-1F1F0-1F1FC { background-position: -2720px 0px; }
-.emoji-1F1F0-1F1FE { background-position: -2740px 0px; }
-.emoji-1F1F0-1F1FF { background-position: -2760px 0px; }
-.emoji-1F1F1-1F1E6 { background-position: -2780px 0px; }
-.emoji-1F1F1-1F1E7 { background-position: -2800px 0px; }
-.emoji-1F1F1-1F1E8 { background-position: -2820px 0px; }
-.emoji-1F1F1-1F1EE { background-position: -2840px 0px; }
-.emoji-1F1F1-1F1F0 { background-position: -2860px 0px; }
-.emoji-1F1F1-1F1F7 { background-position: -2880px 0px; }
-.emoji-1F1F1-1F1F8 { background-position: -2900px 0px; }
-.emoji-1F1F1-1F1F9 { background-position: -2920px 0px; }
-.emoji-1F1F1-1F1FA { background-position: -2940px 0px; }
-.emoji-1F1F1-1F1FB { background-position: -2960px 0px; }
-.emoji-1F1F1-1F1FE { background-position: -2980px 0px; }
-.emoji-1F1F2-1F1E6 { background-position: -3000px 0px; }
-.emoji-1F1F2-1F1E8 { background-position: -3020px 0px; }
-.emoji-1F1F2-1F1E9 { background-position: -3040px 0px; }
-.emoji-1F1F2-1F1EA { background-position: -3060px 0px; }
-.emoji-1F1F2-1F1EC { background-position: -3080px 0px; }
-.emoji-1F1F2-1F1ED { background-position: -3100px 0px; }
-.emoji-1F1F2-1F1F0 { background-position: -3120px 0px; }
-.emoji-1F1F2-1F1F1 { background-position: -3140px 0px; }
-.emoji-1F1F2-1F1F2 { background-position: -3160px 0px; }
-.emoji-1F1F2-1F1F3 { background-position: -3180px 0px; }
-.emoji-1F1F2-1F1F4 { background-position: -3200px 0px; }
-.emoji-1F1F2-1F1F7 { background-position: -3220px 0px; }
-.emoji-1F1F2-1F1F8 { background-position: -3240px 0px; }
-.emoji-1F1F2-1F1F9 { background-position: -3260px 0px; }
-.emoji-1F1F2-1F1FA { background-position: -3280px 0px; }
-.emoji-1F1F2-1F1FB { background-position: -3300px 0px; }
-.emoji-1F1F2-1F1FC { background-position: -3320px 0px; }
-.emoji-1F1F2-1F1FD { background-position: -3340px 0px; }
-.emoji-1F1F2-1F1FE { background-position: -3360px 0px; }
-.emoji-1F1F2-1F1FF { background-position: -3380px 0px; }
-.emoji-1F1F3-1F1E6 { background-position: -3400px 0px; }
-.emoji-1F1F3-1F1E8 { background-position: -3420px 0px; }
-.emoji-1F1F3-1F1EA { background-position: -3440px 0px; }
-.emoji-1F1F3-1F1EC { background-position: -3460px 0px; }
-.emoji-1F1F3-1F1EE { background-position: -3480px 0px; }
-.emoji-1F1F3-1F1F1 { background-position: -3500px 0px; }
-.emoji-1F1F3-1F1F4 { background-position: -3520px 0px; }
-.emoji-1F1F3-1F1F5 { background-position: -3540px 0px; }
-.emoji-1F1F3-1F1F7 { background-position: -3560px 0px; }
-.emoji-1F1F3-1F1FA { background-position: -3580px 0px; }
-.emoji-1F1F3-1F1FF { background-position: -3600px 0px; }
-.emoji-1F1F4-1F1F2 { background-position: -3620px 0px; }
-.emoji-1F1F5-1F1E6 { background-position: -3640px 0px; }
-.emoji-1F1F5-1F1EA { background-position: -3660px 0px; }
-.emoji-1F1F5-1F1EB { background-position: -3680px 0px; }
-.emoji-1F1F5-1F1EC { background-position: -3700px 0px; }
-.emoji-1F1F5-1F1ED { background-position: -3720px 0px; }
-.emoji-1F1F5-1F1F0 { background-position: -3740px 0px; }
-.emoji-1F1F5-1F1F1 { background-position: -3760px 0px; }
-.emoji-1F1F5-1F1F7 { background-position: -3780px 0px; }
-.emoji-1F1F5-1F1F8 { background-position: -3800px 0px; }
-.emoji-1F1F5-1F1F9 { background-position: -3820px 0px; }
-.emoji-1F1F5-1F1FC { background-position: -3840px 0px; }
-.emoji-1F1F5-1F1FE { background-position: -3860px 0px; }
-.emoji-1F1F6-1F1E6 { background-position: -3880px 0px; }
-.emoji-1F1F7-1F1F4 { background-position: -3900px 0px; }
-.emoji-1F1F7-1F1F8 { background-position: -3920px 0px; }
-.emoji-1F1F7-1F1FA { background-position: -3940px 0px; }
-.emoji-1F1F7-1F1FC { background-position: -3960px 0px; }
-.emoji-1F1F8-1F1E6 { background-position: -3980px 0px; }
-.emoji-1F1F8-1F1E7 { background-position: -4000px 0px; }
-.emoji-1F1F8-1F1E8 { background-position: -4020px 0px; }
-.emoji-1F1F8-1F1E9 { background-position: -4040px 0px; }
-.emoji-1F1F8-1F1EA { background-position: -4060px 0px; }
-.emoji-1F1F8-1F1EC { background-position: -4080px 0px; }
-.emoji-1F1F8-1F1ED { background-position: -4100px 0px; }
-.emoji-1F1F8-1F1EE { background-position: -4120px 0px; }
-.emoji-1F1F8-1F1F0 { background-position: -4140px 0px; }
-.emoji-1F1F8-1F1F1 { background-position: -4160px 0px; }
-.emoji-1F1F8-1F1F2 { background-position: -4180px 0px; }
-.emoji-1F1F8-1F1F3 { background-position: -4200px 0px; }
-.emoji-1F1F8-1F1F4 { background-position: -4220px 0px; }
-.emoji-1F1F8-1F1F7 { background-position: -4240px 0px; }
-.emoji-1F1F8-1F1F9 { background-position: -4260px 0px; }
-.emoji-1F1F8-1F1FB { background-position: -4280px 0px; }
-.emoji-1F1F8-1F1FE { background-position: -4300px 0px; }
-.emoji-1F1F8-1F1FF { background-position: -4320px 0px; }
-.emoji-1F1F9-1F1E9 { background-position: -4340px 0px; }
-.emoji-1F1F9-1F1EC { background-position: -4360px 0px; }
-.emoji-1F1F9-1F1ED { background-position: -4380px 0px; }
-.emoji-1F1F9-1F1EF { background-position: -4400px 0px; }
-.emoji-1F1F9-1F1F1 { background-position: -4420px 0px; }
-.emoji-1F1F9-1F1F2 { background-position: -4440px 0px; }
-.emoji-1F1F9-1F1F3 { background-position: -4460px 0px; }
-.emoji-1F1F9-1F1F4 { background-position: -4480px 0px; }
-.emoji-1F1F9-1F1F7 { background-position: -4500px 0px; }
-.emoji-1F1F9-1F1F9 { background-position: -4520px 0px; }
-.emoji-1F1F9-1F1FB { background-position: -4540px 0px; }
-.emoji-1F1F9-1F1FC { background-position: -4560px 0px; }
-.emoji-1F1F9-1F1FF { background-position: -4580px 0px; }
-.emoji-1F1FA-1F1E6 { background-position: -4600px 0px; }
-.emoji-1F1FA-1F1EC { background-position: -4620px 0px; }
-.emoji-1F1FA-1F1F8 { background-position: -4640px 0px; }
-.emoji-1F1FA-1F1FE { background-position: -4660px 0px; }
-.emoji-1F1FA-1F1FF { background-position: -4680px 0px; }
-.emoji-1F1FB-1F1E6 { background-position: -4700px 0px; }
-.emoji-1F1FB-1F1E8 { background-position: -4720px 0px; }
-.emoji-1F1FB-1F1EA { background-position: -4740px 0px; }
-.emoji-1F1FB-1F1EE { background-position: -4760px 0px; }
-.emoji-1F1FB-1F1F3 { background-position: -4780px 0px; }
-.emoji-1F1FB-1F1FA { background-position: -4800px 0px; }
-.emoji-1F1FC-1F1EB { background-position: -4820px 0px; }
-.emoji-1F1FC-1F1F8 { background-position: -4840px 0px; }
-.emoji-1F1FD-1F1F0 { background-position: -4860px 0px; }
-.emoji-1F1FE-1F1EA { background-position: -4880px 0px; }
-.emoji-1F1FF-1F1E6 { background-position: -4900px 0px; }
-.emoji-1F1FF-1F1F2 { background-position: -4920px 0px; }
-.emoji-1F1FF-1F1FC { background-position: -4940px 0px; }
-.emoji-1F201 { background-position: -4960px 0px; }
-.emoji-1F202 { background-position: -4980px 0px; }
-.emoji-1F21A { background-position: -5000px 0px; }
-.emoji-1F22F { background-position: -5020px 0px; }
-.emoji-1F232 { background-position: -5040px 0px; }
-.emoji-1F233 { background-position: -5060px 0px; }
-.emoji-1F234 { background-position: -5080px 0px; }
-.emoji-1F235 { background-position: -5100px 0px; }
-.emoji-1F236 { background-position: -5120px 0px; }
-.emoji-1F237 { background-position: -5140px 0px; }
-.emoji-1F238 { background-position: -5160px 0px; }
-.emoji-1F239 { background-position: -5180px 0px; }
-.emoji-1F23A { background-position: -5200px 0px; }
-.emoji-1F250 { background-position: -5220px 0px; }
-.emoji-1F251 { background-position: -5240px 0px; }
-.emoji-1F300 { background-position: -5260px 0px; }
-.emoji-1F301 { background-position: -5280px 0px; }
-.emoji-1F302 { background-position: -5300px 0px; }
-.emoji-1F303 { background-position: -5320px 0px; }
-.emoji-1F304 { background-position: -5340px 0px; }
-.emoji-1F305 { background-position: -5360px 0px; }
-.emoji-1F306 { background-position: -5380px 0px; }
-.emoji-1F307 { background-position: -5400px 0px; }
-.emoji-1F308 { background-position: -5420px 0px; }
-.emoji-1F309 { background-position: -5440px 0px; }
-.emoji-1F30A { background-position: -5460px 0px; }
-.emoji-1F30B { background-position: -5480px 0px; }
-.emoji-1F30C { background-position: -5500px 0px; }
-.emoji-1F30D { background-position: -5520px 0px; }
-.emoji-1F30E { background-position: -5540px 0px; }
-.emoji-1F30F { background-position: -5560px 0px; }
-.emoji-1F310 { background-position: -5580px 0px; }
-.emoji-1F311 { background-position: -5600px 0px; }
-.emoji-1F312 { background-position: -5620px 0px; }
-.emoji-1F313 { background-position: -5640px 0px; }
-.emoji-1F314 { background-position: -5660px 0px; }
-.emoji-1F315 { background-position: -5680px 0px; }
-.emoji-1F316 { background-position: -5700px 0px; }
-.emoji-1F317 { background-position: -5720px 0px; }
-.emoji-1F318 { background-position: -5740px 0px; }
-.emoji-1F319 { background-position: -5760px 0px; }
-.emoji-1F31A { background-position: -5780px 0px; }
-.emoji-1F31B { background-position: -5800px 0px; }
-.emoji-1F31C { background-position: -5820px 0px; }
-.emoji-1F31D { background-position: -5840px 0px; }
-.emoji-1F31E { background-position: -5860px 0px; }
-.emoji-1F31F { background-position: -5880px 0px; }
-.emoji-1F320 { background-position: -5900px 0px; }
-.emoji-1F321 { background-position: -5920px 0px; }
-.emoji-1F327 { background-position: -5940px 0px; }
-.emoji-1F328 { background-position: -5960px 0px; }
-.emoji-1F329 { background-position: -5980px 0px; }
-.emoji-1F32A { background-position: -6000px 0px; }
-.emoji-1F32B { background-position: -6020px 0px; }
-.emoji-1F32C { background-position: -6040px 0px; }
-.emoji-1F330 { background-position: -6060px 0px; }
-.emoji-1F331 { background-position: -6080px 0px; }
-.emoji-1F332 { background-position: -6100px 0px; }
-.emoji-1F333 { background-position: -6120px 0px; }
-.emoji-1F334 { background-position: -6140px 0px; }
-.emoji-1F335 { background-position: -6160px 0px; }
-.emoji-1F336 { background-position: -6180px 0px; }
-.emoji-1F337 { background-position: -6200px 0px; }
-.emoji-1F338 { background-position: -6220px 0px; }
-.emoji-1F339 { background-position: -6240px 0px; }
-.emoji-1F33A { background-position: -6260px 0px; }
-.emoji-1F33B { background-position: -6280px 0px; }
-.emoji-1F33C { background-position: -6300px 0px; }
-.emoji-1F33D { background-position: -6320px 0px; }
-.emoji-1F33E { background-position: -6340px 0px; }
-.emoji-1F33F { background-position: -6360px 0px; }
-.emoji-1F340 { background-position: -6380px 0px; }
-.emoji-1F341 { background-position: -6400px 0px; }
-.emoji-1F342 { background-position: -6420px 0px; }
-.emoji-1F343 { background-position: -6440px 0px; }
-.emoji-1F344 { background-position: -6460px 0px; }
-.emoji-1F345 { background-position: -6480px 0px; }
-.emoji-1F346 { background-position: -6500px 0px; }
-.emoji-1F347 { background-position: -6520px 0px; }
-.emoji-1F348 { background-position: -6540px 0px; }
-.emoji-1F349 { background-position: -6560px 0px; }
-.emoji-1F34A { background-position: -6580px 0px; }
-.emoji-1F34B { background-position: -6600px 0px; }
-.emoji-1F34C { background-position: -6620px 0px; }
-.emoji-1F34D { background-position: -6640px 0px; }
-.emoji-1F34E { background-position: -6660px 0px; }
-.emoji-1F34F { background-position: -6680px 0px; }
-.emoji-1F350 { background-position: -6700px 0px; }
-.emoji-1F351 { background-position: -6720px 0px; }
-.emoji-1F352 { background-position: -6740px 0px; }
-.emoji-1F353 { background-position: -6760px 0px; }
-.emoji-1F354 { background-position: -6780px 0px; }
-.emoji-1F355 { background-position: -6800px 0px; }
-.emoji-1F356 { background-position: -6820px 0px; }
-.emoji-1F357 { background-position: -6840px 0px; }
-.emoji-1F358 { background-position: -6860px 0px; }
-.emoji-1F359 { background-position: -6880px 0px; }
-.emoji-1F35A { background-position: -6900px 0px; }
-.emoji-1F35B { background-position: -6920px 0px; }
-.emoji-1F35C { background-position: -6940px 0px; }
-.emoji-1F35D { background-position: -6960px 0px; }
-.emoji-1F35E { background-position: -6980px 0px; }
-.emoji-1F35F { background-position: -7000px 0px; }
-.emoji-1F360 { background-position: -7020px 0px; }
-.emoji-1F361 { background-position: -7040px 0px; }
-.emoji-1F362 { background-position: -7060px 0px; }
-.emoji-1F363 { background-position: -7080px 0px; }
-.emoji-1F364 { background-position: -7100px 0px; }
-.emoji-1F365 { background-position: -7120px 0px; }
-.emoji-1F366 { background-position: -7140px 0px; }
-.emoji-1F367 { background-position: -7160px 0px; }
-.emoji-1F368 { background-position: -7180px 0px; }
-.emoji-1F369 { background-position: -7200px 0px; }
-.emoji-1F36A { background-position: -7220px 0px; }
-.emoji-1F36B { background-position: -7240px 0px; }
-.emoji-1F36C { background-position: -7260px 0px; }
-.emoji-1F36D { background-position: -7280px 0px; }
-.emoji-1F36E { background-position: -7300px 0px; }
-.emoji-1F36F { background-position: -7320px 0px; }
-.emoji-1F370 { background-position: -7340px 0px; }
-.emoji-1F371 { background-position: -7360px 0px; }
-.emoji-1F372 { background-position: -7380px 0px; }
-.emoji-1F373 { background-position: -7400px 0px; }
-.emoji-1F374 { background-position: -7420px 0px; }
-.emoji-1F375 { background-position: -7440px 0px; }
-.emoji-1F376 { background-position: -7460px 0px; }
-.emoji-1F377 { background-position: -7480px 0px; }
-.emoji-1F378 { background-position: -7500px 0px; }
-.emoji-1F379 { background-position: -7520px 0px; }
-.emoji-1F37A { background-position: -7540px 0px; }
-.emoji-1F37B { background-position: -7560px 0px; }
-.emoji-1F37C { background-position: -7580px 0px; }
-.emoji-1F37D { background-position: -7600px 0px; }
-.emoji-1F380 { background-position: -7620px 0px; }
-.emoji-1F381 { background-position: -7640px 0px; }
-.emoji-1F382 { background-position: -7660px 0px; }
-.emoji-1F383 { background-position: -7680px 0px; }
-.emoji-1F384 { background-position: -7700px 0px; }
-.emoji-1F385 { background-position: -7720px 0px; }
-.emoji-1F386 { background-position: -7740px 0px; }
-.emoji-1F387 { background-position: -7760px 0px; }
-.emoji-1F388 { background-position: -7780px 0px; }
-.emoji-1F389 { background-position: -7800px 0px; }
-.emoji-1F38A { background-position: -7820px 0px; }
-.emoji-1F38B { background-position: -7840px 0px; }
-.emoji-1F38C { background-position: -7860px 0px; }
-.emoji-1F38D { background-position: -7880px 0px; }
-.emoji-1F38E { background-position: -7900px 0px; }
-.emoji-1F38F { background-position: -7920px 0px; }
-.emoji-1F390 { background-position: -7940px 0px; }
-.emoji-1F391 { background-position: -7960px 0px; }
-.emoji-1F392 { background-position: -7980px 0px; }
-.emoji-1F393 { background-position: -8000px 0px; }
-.emoji-1F394 { background-position: -8020px 0px; }
-.emoji-1F395 { background-position: -8040px 0px; }
-.emoji-1F396 { background-position: -8060px 0px; }
-.emoji-1F397 { background-position: -8080px 0px; }
-.emoji-1F398 { background-position: -8100px 0px; }
-.emoji-1F399 { background-position: -8120px 0px; }
-.emoji-1F39A { background-position: -8140px 0px; }
-.emoji-1F39B { background-position: -8160px 0px; }
-.emoji-1F39C { background-position: -8180px 0px; }
-.emoji-1F39D { background-position: -8200px 0px; }
-.emoji-1F39E { background-position: -8220px 0px; }
-.emoji-1F39F { background-position: -8240px 0px; }
-.emoji-1F3A0 { background-position: -8260px 0px; }
-.emoji-1F3A1 { background-position: -8280px 0px; }
-.emoji-1F3A2 { background-position: -8300px 0px; }
-.emoji-1F3A3 { background-position: -8320px 0px; }
-.emoji-1F3A4 { background-position: -8340px 0px; }
-.emoji-1F3A5 { background-position: -8360px 0px; }
-.emoji-1F3A6 { background-position: -8380px 0px; }
-.emoji-1F3A7 { background-position: -8400px 0px; }
-.emoji-1F3A8 { background-position: -8420px 0px; }
-.emoji-1F3A9 { background-position: -8440px 0px; }
-.emoji-1F3AA { background-position: -8460px 0px; }
-.emoji-1F3AB { background-position: -8480px 0px; }
-.emoji-1F3AC { background-position: -8500px 0px; }
-.emoji-1F3AD { background-position: -8520px 0px; }
-.emoji-1F3AE { background-position: -8540px 0px; }
-.emoji-1F3AF { background-position: -8560px 0px; }
-.emoji-1F3B0 { background-position: -8580px 0px; }
-.emoji-1F3B1 { background-position: -8600px 0px; }
-.emoji-1F3B2 { background-position: -8620px 0px; }
-.emoji-1F3B3 { background-position: -8640px 0px; }
-.emoji-1F3B4 { background-position: -8660px 0px; }
-.emoji-1F3B5 { background-position: -8680px 0px; }
-.emoji-1F3B6 { background-position: -8700px 0px; }
-.emoji-1F3B7 { background-position: -8720px 0px; }
-.emoji-1F3B8 { background-position: -8740px 0px; }
-.emoji-1F3B9 { background-position: -8760px 0px; }
-.emoji-1F3BA { background-position: -8780px 0px; }
-.emoji-1F3BB { background-position: -8800px 0px; }
-.emoji-1F3BC { background-position: -8820px 0px; }
-.emoji-1F3BD { background-position: -8840px 0px; }
-.emoji-1F3BE { background-position: -8860px 0px; }
-.emoji-1F3BF { background-position: -8880px 0px; }
-.emoji-1F3C0 { background-position: -8900px 0px; }
-.emoji-1F3C1 { background-position: -8920px 0px; }
-.emoji-1F3C2 { background-position: -8940px 0px; }
-.emoji-1F3C3 { background-position: -8960px 0px; }
-.emoji-1F3C4 { background-position: -8980px 0px; }
-.emoji-1F3C5 { background-position: -9000px 0px; }
-.emoji-1F3C6 { background-position: -9020px 0px; }
-.emoji-1F3C7 { background-position: -9040px 0px; }
-.emoji-1F3C8 { background-position: -9060px 0px; }
-.emoji-1F3C9 { background-position: -9080px 0px; }
-.emoji-1F3CA { background-position: -9100px 0px; }
-.emoji-1F3CB { background-position: -9120px 0px; }
-.emoji-1F3CC { background-position: -9140px 0px; }
-.emoji-1F3CD { background-position: -9160px 0px; }
-.emoji-1F3CE { background-position: -9180px 0px; }
-.emoji-1F3D4 { background-position: -9200px 0px; }
-.emoji-1F3D5 { background-position: -9220px 0px; }
-.emoji-1F3D6 { background-position: -9240px 0px; }
-.emoji-1F3D7 { background-position: -9260px 0px; }
-.emoji-1F3D8 { background-position: -9280px 0px; }
-.emoji-1F3D9 { background-position: -9300px 0px; }
-.emoji-1F3DA { background-position: -9320px 0px; }
-.emoji-1F3DB { background-position: -9340px 0px; }
-.emoji-1F3DC { background-position: -9360px 0px; }
-.emoji-1F3DD { background-position: -9380px 0px; }
-.emoji-1F3DE { background-position: -9400px 0px; }
-.emoji-1F3DF { background-position: -9420px 0px; }
-.emoji-1F3E0 { background-position: -9440px 0px; }
-.emoji-1F3E1 { background-position: -9460px 0px; }
-.emoji-1F3E2 { background-position: -9480px 0px; }
-.emoji-1F3E3 { background-position: -9500px 0px; }
-.emoji-1F3E4 { background-position: -9520px 0px; }
-.emoji-1F3E5 { background-position: -9540px 0px; }
-.emoji-1F3E6 { background-position: -9560px 0px; }
-.emoji-1F3E7 { background-position: -9580px 0px; }
-.emoji-1F3E8 { background-position: -9600px 0px; }
-.emoji-1F3E9 { background-position: -9620px 0px; }
-.emoji-1F3EA { background-position: -9640px 0px; }
-.emoji-1F3EB { background-position: -9660px 0px; }
-.emoji-1F3EC { background-position: -9680px 0px; }
-.emoji-1F3ED { background-position: -9700px 0px; }
-.emoji-1F3EE { background-position: -9720px 0px; }
-.emoji-1F3EF { background-position: -9740px 0px; }
-.emoji-1F3F0 { background-position: -9760px 0px; }
-.emoji-1F3F1 { background-position: -9780px 0px; }
-.emoji-1F3F2 { background-position: -9800px 0px; }
-.emoji-1F3F3 { background-position: -9820px 0px; }
-.emoji-1F3F4 { background-position: -9840px 0px; }
-.emoji-1F3F5 { background-position: -9860px 0px; }
-.emoji-1F3F6 { background-position: -9880px 0px; }
-.emoji-1F3F7 { background-position: -9900px 0px; }
-.emoji-1F400 { background-position: -9920px 0px; }
-.emoji-1F401 { background-position: -9940px 0px; }
-.emoji-1F402 { background-position: -9960px 0px; }
-.emoji-1F403 { background-position: -9980px 0px; }
-.emoji-1F404 { background-position: -10000px 0px; }
-.emoji-1F405 { background-position: -10020px 0px; }
-.emoji-1F406 { background-position: -10040px 0px; }
-.emoji-1F407 { background-position: -10060px 0px; }
-.emoji-1F408 { background-position: -10080px 0px; }
-.emoji-1F409 { background-position: -10100px 0px; }
-.emoji-1F40A { background-position: -10120px 0px; }
-.emoji-1F40B { background-position: -10140px 0px; }
-.emoji-1F40C { background-position: -10160px 0px; }
-.emoji-1F40D { background-position: -10180px 0px; }
-.emoji-1F40E { background-position: -10200px 0px; }
-.emoji-1F40F { background-position: -10220px 0px; }
-.emoji-1F410 { background-position: -10240px 0px; }
-.emoji-1F411 { background-position: -10260px 0px; }
-.emoji-1F412 { background-position: -10280px 0px; }
-.emoji-1F413 { background-position: -10300px 0px; }
-.emoji-1F414 { background-position: -10320px 0px; }
-.emoji-1F415 { background-position: -10340px 0px; }
-.emoji-1F416 { background-position: -10360px 0px; }
-.emoji-1F417 { background-position: -10380px 0px; }
-.emoji-1F418 { background-position: -10400px 0px; }
-.emoji-1F419 { background-position: -10420px 0px; }
-.emoji-1F41A { background-position: -10440px 0px; }
-.emoji-1F41B { background-position: -10460px 0px; }
-.emoji-1F41C { background-position: -10480px 0px; }
-.emoji-1F41D { background-position: -10500px 0px; }
-.emoji-1F41E { background-position: -10520px 0px; }
-.emoji-1F41F { background-position: -10540px 0px; }
-.emoji-1F420 { background-position: -10560px 0px; }
-.emoji-1F421 { background-position: -10580px 0px; }
-.emoji-1F422 { background-position: -10600px 0px; }
-.emoji-1F423 { background-position: -10620px 0px; }
-.emoji-1F424 { background-position: -10640px 0px; }
-.emoji-1F425 { background-position: -10660px 0px; }
-.emoji-1F426 { background-position: -10680px 0px; }
-.emoji-1F427 { background-position: -10700px 0px; }
-.emoji-1F428 { background-position: -10720px 0px; }
-.emoji-1F429 { background-position: -10740px 0px; }
-.emoji-1F42A { background-position: -10760px 0px; }
-.emoji-1F42B { background-position: -10780px 0px; }
-.emoji-1F42C { background-position: -10800px 0px; }
-.emoji-1F42D { background-position: -10820px 0px; }
-.emoji-1F42E { background-position: -10840px 0px; }
-.emoji-1F42F { background-position: -10860px 0px; }
-.emoji-1F430 { background-position: -10880px 0px; }
-.emoji-1F431 { background-position: -10900px 0px; }
-.emoji-1F432 { background-position: -10920px 0px; }
-.emoji-1F433 { background-position: -10940px 0px; }
-.emoji-1F434 { background-position: -10960px 0px; }
-.emoji-1F435 { background-position: -10980px 0px; }
-.emoji-1F436 { background-position: -11000px 0px; }
-.emoji-1F437 { background-position: -11020px 0px; }
-.emoji-1F438 { background-position: -11040px 0px; }
-.emoji-1F439 { background-position: -11060px 0px; }
-.emoji-1F43A { background-position: -11080px 0px; }
-.emoji-1F43B { background-position: -11100px 0px; }
-.emoji-1F43C { background-position: -11120px 0px; }
-.emoji-1F43D { background-position: -11140px 0px; }
-.emoji-1F43E { background-position: -11160px 0px; }
-.emoji-1F43F { background-position: -11180px 0px; }
-.emoji-1F440 { background-position: -11200px 0px; }
-.emoji-1F441 { background-position: -11220px 0px; }
-.emoji-1F442 { background-position: -11240px 0px; }
-.emoji-1F443 { background-position: -11260px 0px; }
-.emoji-1F444 { background-position: -11280px 0px; }
-.emoji-1F445 { background-position: -11300px 0px; }
-.emoji-1F446 { background-position: -11320px 0px; }
-.emoji-1F447 { background-position: -11340px 0px; }
-.emoji-1F448 { background-position: -11360px 0px; }
-.emoji-1F449 { background-position: -11380px 0px; }
-.emoji-1F44A { background-position: -11400px 0px; }
-.emoji-1F44B { background-position: -11420px 0px; }
-.emoji-1F44C { background-position: -11440px 0px; }
-.emoji-1F44D { background-position: -11460px 0px; }
-.emoji-1F44E { background-position: -11480px 0px; }
-.emoji-1F44F { background-position: -11500px 0px; }
-.emoji-1F450 { background-position: -11520px 0px; }
-.emoji-1F451 { background-position: -11540px 0px; }
-.emoji-1F452 { background-position: -11560px 0px; }
-.emoji-1F453 { background-position: -11580px 0px; }
-.emoji-1F454 { background-position: -11600px 0px; }
-.emoji-1F455 { background-position: -11620px 0px; }
-.emoji-1F456 { background-position: -11640px 0px; }
-.emoji-1F457 { background-position: -11660px 0px; }
-.emoji-1F458 { background-position: -11680px 0px; }
-.emoji-1F459 { background-position: -11700px 0px; }
-.emoji-1F45A { background-position: -11720px 0px; }
-.emoji-1F45B { background-position: -11740px 0px; }
-.emoji-1F45C { background-position: -11760px 0px; }
-.emoji-1F45D { background-position: -11780px 0px; }
-.emoji-1F45E { background-position: -11800px 0px; }
-.emoji-1F45F { background-position: -11820px 0px; }
-.emoji-1F460 { background-position: -11840px 0px; }
-.emoji-1F461 { background-position: -11860px 0px; }
-.emoji-1F462 { background-position: -11880px 0px; }
-.emoji-1F463 { background-position: -11900px 0px; }
-.emoji-1F464 { background-position: -11920px 0px; }
-.emoji-1F465 { background-position: -11940px 0px; }
-.emoji-1F466 { background-position: -11960px 0px; }
-.emoji-1F467 { background-position: -11980px 0px; }
-.emoji-1F468 { background-position: -12000px 0px; }
-.emoji-1F468-1F468-1F466 { background-position: -12020px 0px; }
-.emoji-1F468-1F468-1F466-1F466 { background-position: -12040px 0px; }
-.emoji-1F468-1F468-1F467 { background-position: -12060px 0px; }
-.emoji-1F468-1F468-1F467-1F466 { background-position: -12080px 0px; }
-.emoji-1F468-1F468-1F467-1F467 { background-position: -12100px 0px; }
-.emoji-1F468-1F469-1F466-1F466 { background-position: -12120px 0px; }
-.emoji-1F468-1F469-1F467 { background-position: -12140px 0px; }
-.emoji-1F468-1F469-1F467-1F466 { background-position: -12160px 0px; }
-.emoji-1F468-1F469-1F467-1F467 { background-position: -12180px 0px; }
-.emoji-1F468-2764-1F468 { background-position: -12200px 0px; }
-.emoji-1F468-2764-1F48B-1F468 { background-position: -12220px 0px; }
-.emoji-1F469 { background-position: -12240px 0px; }
-.emoji-1F469-1F469-1F466 { background-position: -12260px 0px; }
-.emoji-1F469-1F469-1F466-1F466 { background-position: -12280px 0px; }
-.emoji-1F469-1F469-1F467 { background-position: -12300px 0px; }
-.emoji-1F469-1F469-1F467-1F466 { background-position: -12320px 0px; }
-.emoji-1F469-1F469-1F467-1F467 { background-position: -12340px 0px; }
-.emoji-1F469-2764-1F469 { background-position: -12360px 0px; }
-.emoji-1F469-2764-1F48B-1F469 { background-position: -12380px 0px; }
-.emoji-1F46A { background-position: -12400px 0px; }
-.emoji-1F46B { background-position: -12420px 0px; }
-.emoji-1F46C { background-position: -12440px 0px; }
-.emoji-1F46D { background-position: -12460px 0px; }
-.emoji-1F46E { background-position: -12480px 0px; }
-.emoji-1F46F { background-position: -12500px 0px; }
-.emoji-1F470 { background-position: -12520px 0px; }
-.emoji-1F471 { background-position: -12540px 0px; }
-.emoji-1F472 { background-position: -12560px 0px; }
-.emoji-1F473 { background-position: -12580px 0px; }
-.emoji-1F474 { background-position: -12600px 0px; }
-.emoji-1F475 { background-position: -12620px 0px; }
-.emoji-1F476 { background-position: -12640px 0px; }
-.emoji-1F477 { background-position: -12660px 0px; }
-.emoji-1F478 { background-position: -12680px 0px; }
-.emoji-1F479 { background-position: -12700px 0px; }
-.emoji-1F47A { background-position: -12720px 0px; }
-.emoji-1F47B { background-position: -12740px 0px; }
-.emoji-1F47C { background-position: -12760px 0px; }
-.emoji-1F47D { background-position: -12780px 0px; }
-.emoji-1F47E { background-position: -12800px 0px; }
-.emoji-1F47F { background-position: -12820px 0px; }
-.emoji-1F480 { background-position: -12840px 0px; }
-.emoji-1F481 { background-position: -12860px 0px; }
-.emoji-1F482 { background-position: -12880px 0px; }
-.emoji-1F483 { background-position: -12900px 0px; }
-.emoji-1F484 { background-position: -12920px 0px; }
-.emoji-1F485 { background-position: -12940px 0px; }
-.emoji-1F486 { background-position: -12960px 0px; }
-.emoji-1F487 { background-position: -12980px 0px; }
-.emoji-1F488 { background-position: -13000px 0px; }
-.emoji-1F489 { background-position: -13020px 0px; }
-.emoji-1F48A { background-position: -13040px 0px; }
-.emoji-1F48B { background-position: -13060px 0px; }
-.emoji-1F48C { background-position: -13080px 0px; }
-.emoji-1F48D { background-position: -13100px 0px; }
-.emoji-1F48E { background-position: -13120px 0px; }
-.emoji-1F48F { background-position: -13140px 0px; }
-.emoji-1F490 { background-position: -13160px 0px; }
-.emoji-1F491 { background-position: -13180px 0px; }
-.emoji-1F492 { background-position: -13200px 0px; }
-.emoji-1F493 { background-position: -13220px 0px; }
-.emoji-1F494 { background-position: -13240px 0px; }
-.emoji-1F495 { background-position: -13260px 0px; }
-.emoji-1F496 { background-position: -13280px 0px; }
-.emoji-1F497 { background-position: -13300px 0px; }
-.emoji-1F498 { background-position: -13320px 0px; }
-.emoji-1F499 { background-position: -13340px 0px; }
-.emoji-1F49A { background-position: -13360px 0px; }
-.emoji-1F49B { background-position: -13380px 0px; }
-.emoji-1F49C { background-position: -13400px 0px; }
-.emoji-1F49D { background-position: -13420px 0px; }
-.emoji-1F49E { background-position: -13440px 0px; }
-.emoji-1F49F { background-position: -13460px 0px; }
-.emoji-1F4A0 { background-position: -13480px 0px; }
-.emoji-1F4A1 { background-position: -13500px 0px; }
-.emoji-1F4A2 { background-position: -13520px 0px; }
-.emoji-1F4A3 { background-position: -13540px 0px; }
-.emoji-1F4A4 { background-position: -13560px 0px; }
-.emoji-1F4A5 { background-position: -13580px 0px; }
-.emoji-1F4A6 { background-position: -13600px 0px; }
-.emoji-1F4A7 { background-position: -13620px 0px; }
-.emoji-1F4A8 { background-position: -13640px 0px; }
-.emoji-1F4A9 { background-position: -13660px 0px; }
-.emoji-1F4AA { background-position: -13680px 0px; }
-.emoji-1F4AB { background-position: -13700px 0px; }
-.emoji-1F4AC { background-position: -13720px 0px; }
-.emoji-1F4AD { background-position: -13740px 0px; }
-.emoji-1F4AE { background-position: -13760px 0px; }
-.emoji-1F4AF { background-position: -13780px 0px; }
-.emoji-1F4B0 { background-position: -13800px 0px; }
-.emoji-1F4B1 { background-position: -13820px 0px; }
-.emoji-1F4B2 { background-position: -13840px 0px; }
-.emoji-1F4B3 { background-position: -13860px 0px; }
-.emoji-1F4B4 { background-position: -13880px 0px; }
-.emoji-1F4B5 { background-position: -13900px 0px; }
-.emoji-1F4B6 { background-position: -13920px 0px; }
-.emoji-1F4B7 { background-position: -13940px 0px; }
-.emoji-1F4B8 { background-position: -13960px 0px; }
-.emoji-1F4B9 { background-position: -13980px 0px; }
-.emoji-1F4BA { background-position: -14000px 0px; }
-.emoji-1F4BB { background-position: -14020px 0px; }
-.emoji-1F4BC { background-position: -14040px 0px; }
-.emoji-1F4BD { background-position: -14060px 0px; }
-.emoji-1F4BE { background-position: -14080px 0px; }
-.emoji-1F4BF { background-position: -14100px 0px; }
-.emoji-1F4C0 { background-position: -14120px 0px; }
-.emoji-1F4C1 { background-position: -14140px 0px; }
-.emoji-1F4C2 { background-position: -14160px 0px; }
-.emoji-1F4C3 { background-position: -14180px 0px; }
-.emoji-1F4C4 { background-position: -14200px 0px; }
-.emoji-1F4C5 { background-position: -14220px 0px; }
-.emoji-1F4C6 { background-position: -14240px 0px; }
-.emoji-1F4C7 { background-position: -14260px 0px; }
-.emoji-1F4C8 { background-position: -14280px 0px; }
-.emoji-1F4C9 { background-position: -14300px 0px; }
-.emoji-1F4CA { background-position: -14320px 0px; }
-.emoji-1F4CB { background-position: -14340px 0px; }
-.emoji-1F4CC { background-position: -14360px 0px; }
-.emoji-1F4CD { background-position: -14380px 0px; }
-.emoji-1F4CE { background-position: -14400px 0px; }
-.emoji-1F4CF { background-position: -14420px 0px; }
-.emoji-1F4D0 { background-position: -14440px 0px; }
-.emoji-1F4D1 { background-position: -14460px 0px; }
-.emoji-1F4D2 { background-position: -14480px 0px; }
-.emoji-1F4D3 { background-position: -14500px 0px; }
-.emoji-1F4D4 { background-position: -14520px 0px; }
-.emoji-1F4D5 { background-position: -14540px 0px; }
-.emoji-1F4D6 { background-position: -14560px 0px; }
-.emoji-1F4D7 { background-position: -14580px 0px; }
-.emoji-1F4D8 { background-position: -14600px 0px; }
-.emoji-1F4D9 { background-position: -14620px 0px; }
-.emoji-1F4DA { background-position: -14640px 0px; }
-.emoji-1F4DB { background-position: -14660px 0px; }
-.emoji-1F4DC { background-position: -14680px 0px; }
-.emoji-1F4DD { background-position: -14700px 0px; }
-.emoji-1F4DE { background-position: -14720px 0px; }
-.emoji-1F4DF { background-position: -14740px 0px; }
-.emoji-1F4E0 { background-position: -14760px 0px; }
-.emoji-1F4E1 { background-position: -14780px 0px; }
-.emoji-1F4E2 { background-position: -14800px 0px; }
-.emoji-1F4E3 { background-position: -14820px 0px; }
-.emoji-1F4E4 { background-position: -14840px 0px; }
-.emoji-1F4E5 { background-position: -14860px 0px; }
-.emoji-1F4E6 { background-position: -14880px 0px; }
-.emoji-1F4E7 { background-position: -14900px 0px; }
-.emoji-1F4E8 { background-position: -14920px 0px; }
-.emoji-1F4E9 { background-position: -14940px 0px; }
-.emoji-1F4EA { background-position: -14960px 0px; }
-.emoji-1F4EB { background-position: -14980px 0px; }
-.emoji-1F4EC { background-position: -15000px 0px; }
-.emoji-1F4ED { background-position: -15020px 0px; }
-.emoji-1F4EE { background-position: -15040px 0px; }
-.emoji-1F4EF { background-position: -15060px 0px; }
-.emoji-1F4F0 { background-position: -15080px 0px; }
-.emoji-1F4F1 { background-position: -15100px 0px; }
-.emoji-1F4F2 { background-position: -15120px 0px; }
-.emoji-1F4F3 { background-position: -15140px 0px; }
-.emoji-1F4F4 { background-position: -15160px 0px; }
-.emoji-1F4F5 { background-position: -15180px 0px; }
-.emoji-1F4F6 { background-position: -15200px 0px; }
-.emoji-1F4F7 { background-position: -15220px 0px; }
-.emoji-1F4F8 { background-position: -15240px 0px; }
-.emoji-1F4F9 { background-position: -15260px 0px; }
-.emoji-1F4FA { background-position: -15280px 0px; }
-.emoji-1F4FB { background-position: -15300px 0px; }
-.emoji-1F4FC { background-position: -15320px 0px; }
-.emoji-1F4FD { background-position: -15340px 0px; }
-.emoji-1F4FE { background-position: -15360px 0px; }
-.emoji-1F500 { background-position: -15380px 0px; }
-.emoji-1F501 { background-position: -15400px 0px; }
-.emoji-1F502 { background-position: -15420px 0px; }
-.emoji-1F503 { background-position: -15440px 0px; }
-.emoji-1F504 { background-position: -15460px 0px; }
-.emoji-1F505 { background-position: -15480px 0px; }
-.emoji-1F506 { background-position: -15500px 0px; }
-.emoji-1F507 { background-position: -15520px 0px; }
-.emoji-1F508 { background-position: -15540px 0px; }
-.emoji-1F509 { background-position: -15560px 0px; }
-.emoji-1F50A { background-position: -15580px 0px; }
-.emoji-1F50B { background-position: -15600px 0px; }
-.emoji-1F50C { background-position: -15620px 0px; }
-.emoji-1F50D { background-position: -15640px 0px; }
-.emoji-1F50E { background-position: -15660px 0px; }
-.emoji-1F50F { background-position: -15680px 0px; }
-.emoji-1F510 { background-position: -15700px 0px; }
-.emoji-1F511 { background-position: -15720px 0px; }
-.emoji-1F512 { background-position: -15740px 0px; }
-.emoji-1F513 { background-position: -15760px 0px; }
-.emoji-1F514 { background-position: -15780px 0px; }
-.emoji-1F515 { background-position: -15800px 0px; }
-.emoji-1F516 { background-position: -15820px 0px; }
-.emoji-1F517 { background-position: -15840px 0px; }
-.emoji-1F518 { background-position: -15860px 0px; }
-.emoji-1F519 { background-position: -15880px 0px; }
-.emoji-1F51A { background-position: -15900px 0px; }
-.emoji-1F51B { background-position: -15920px 0px; }
-.emoji-1F51C { background-position: -15940px 0px; }
-.emoji-1F51D { background-position: -15960px 0px; }
-.emoji-1F51E { background-position: -15980px 0px; }
-.emoji-1F51F { background-position: -16000px 0px; }
-.emoji-1F520 { background-position: -16020px 0px; }
-.emoji-1F521 { background-position: -16040px 0px; }
-.emoji-1F522 { background-position: -16060px 0px; }
-.emoji-1F523 { background-position: -16080px 0px; }
-.emoji-1F524 { background-position: -16100px 0px; }
-.emoji-1F525 { background-position: -16120px 0px; }
-.emoji-1F526 { background-position: -16140px 0px; }
-.emoji-1F527 { background-position: -16160px 0px; }
-.emoji-1F528 { background-position: -16180px 0px; }
-.emoji-1F529 { background-position: -16200px 0px; }
-.emoji-1F52A { background-position: -16220px 0px; }
-.emoji-1F52B { background-position: -16240px 0px; }
-.emoji-1F52C { background-position: -16260px 0px; }
-.emoji-1F52D { background-position: -16280px 0px; }
-.emoji-1F52E { background-position: -16300px 0px; }
-.emoji-1F52F { background-position: -16320px 0px; }
-.emoji-1F530 { background-position: -16340px 0px; }
-.emoji-1F531 { background-position: -16360px 0px; }
-.emoji-1F532 { background-position: -16380px 0px; }
-.emoji-1F533 { background-position: -16400px 0px; }
-.emoji-1F534 { background-position: -16420px 0px; }
-.emoji-1F535 { background-position: -16440px 0px; }
-.emoji-1F536 { background-position: -16460px 0px; }
-.emoji-1F537 { background-position: -16480px 0px; }
-.emoji-1F538 { background-position: -16500px 0px; }
-.emoji-1F539 { background-position: -16520px 0px; }
-.emoji-1F53A { background-position: -16540px 0px; }
-.emoji-1F53B { background-position: -16560px 0px; }
-.emoji-1F53C { background-position: -16580px 0px; }
-.emoji-1F53D { background-position: -16600px 0px; }
-.emoji-1F546 { background-position: -16620px 0px; }
-.emoji-1F547 { background-position: -16640px 0px; }
-.emoji-1F548 { background-position: -16660px 0px; }
-.emoji-1F549 { background-position: -16680px 0px; }
-.emoji-1F54A { background-position: -16700px 0px; }
-.emoji-1F550 { background-position: -16720px 0px; }
-.emoji-1F551 { background-position: -16740px 0px; }
-.emoji-1F552 { background-position: -16760px 0px; }
-.emoji-1F553 { background-position: -16780px 0px; }
-.emoji-1F554 { background-position: -16800px 0px; }
-.emoji-1F555 { background-position: -16820px 0px; }
-.emoji-1F556 { background-position: -16840px 0px; }
-.emoji-1F557 { background-position: -16860px 0px; }
-.emoji-1F558 { background-position: -16880px 0px; }
-.emoji-1F559 { background-position: -16900px 0px; }
-.emoji-1F55A { background-position: -16920px 0px; }
-.emoji-1F55B { background-position: -16940px 0px; }
-.emoji-1F55C { background-position: -16960px 0px; }
-.emoji-1F55D { background-position: -16980px 0px; }
-.emoji-1F55E { background-position: -17000px 0px; }
-.emoji-1F55F { background-position: -17020px 0px; }
-.emoji-1F560 { background-position: -17040px 0px; }
-.emoji-1F561 { background-position: -17060px 0px; }
-.emoji-1F562 { background-position: -17080px 0px; }
-.emoji-1F563 { background-position: -17100px 0px; }
-.emoji-1F564 { background-position: -17120px 0px; }
-.emoji-1F565 { background-position: -17140px 0px; }
-.emoji-1F566 { background-position: -17160px 0px; }
-.emoji-1F567 { background-position: -17180px 0px; }
-.emoji-1F568 { background-position: -17200px 0px; }
-.emoji-1F569 { background-position: -17220px 0px; }
-.emoji-1F56A { background-position: -17240px 0px; }
-.emoji-1F56B { background-position: -17260px 0px; }
-.emoji-1F56C { background-position: -17280px 0px; }
-.emoji-1F56D { background-position: -17300px 0px; }
-.emoji-1F56E { background-position: -17320px 0px; }
-.emoji-1F56F { background-position: -17340px 0px; }
-.emoji-1F570 { background-position: -17360px 0px; }
-.emoji-1F571 { background-position: -17380px 0px; }
-.emoji-1F572 { background-position: -17400px 0px; }
-.emoji-1F573 { background-position: -17420px 0px; }
-.emoji-1F574 { background-position: -17440px 0px; }
-.emoji-1F575 { background-position: -17460px 0px; }
-.emoji-1F576 { background-position: -17480px 0px; }
-.emoji-1F577 { background-position: -17500px 0px; }
-.emoji-1F578 { background-position: -17520px 0px; }
-.emoji-1F579 { background-position: -17540px 0px; }
-.emoji-1F57B { background-position: -17560px 0px; }
-.emoji-1F57E { background-position: -17580px 0px; }
-.emoji-1F57F { background-position: -17600px 0px; }
-.emoji-1F581 { background-position: -17620px 0px; }
-.emoji-1F582 { background-position: -17640px 0px; }
-.emoji-1F583 { background-position: -17660px 0px; }
-.emoji-1F585 { background-position: -17680px 0px; }
-.emoji-1F586 { background-position: -17700px 0px; }
-.emoji-1F587 { background-position: -17720px 0px; }
-.emoji-1F588 { background-position: -17740px 0px; }
-.emoji-1F589 { background-position: -17760px 0px; }
-.emoji-1F58A { background-position: -17780px 0px; }
-.emoji-1F58B { background-position: -17800px 0px; }
-.emoji-1F58C { background-position: -17820px 0px; }
-.emoji-1F58D { background-position: -17840px 0px; }
-.emoji-1F58E { background-position: -17860px 0px; }
-.emoji-1F58F { background-position: -17880px 0px; }
-.emoji-1F590 { background-position: -17900px 0px; }
-.emoji-1F591 { background-position: -17920px 0px; }
-.emoji-1F592 { background-position: -17940px 0px; }
-.emoji-1F593 { background-position: -17960px 0px; }
-.emoji-1F594 { background-position: -17980px 0px; }
-.emoji-1F595 { background-position: -18000px 0px; }
-.emoji-1F596 { background-position: -18020px 0px; }
-.emoji-1F597 { background-position: -18040px 0px; }
-.emoji-1F598 { background-position: -18060px 0px; }
-.emoji-1F599 { background-position: -18080px 0px; }
-.emoji-1F59E { background-position: -18100px 0px; }
-.emoji-1F59F { background-position: -18120px 0px; }
-.emoji-1F5A5 { background-position: -18140px 0px; }
-.emoji-1F5A6 { background-position: -18160px 0px; }
-.emoji-1F5A7 { background-position: -18180px 0px; }
-.emoji-1F5A8 { background-position: -18200px 0px; }
-.emoji-1F5A9 { background-position: -18220px 0px; }
-.emoji-1F5AA { background-position: -18240px 0px; }
-.emoji-1F5AB { background-position: -18260px 0px; }
-.emoji-1F5AD { background-position: -18280px 0px; }
-.emoji-1F5AE { background-position: -18300px 0px; }
-.emoji-1F5AF { background-position: -18320px 0px; }
-.emoji-1F5B2 { background-position: -18340px 0px; }
-.emoji-1F5B3 { background-position: -18360px 0px; }
-.emoji-1F5B4 { background-position: -18380px 0px; }
-.emoji-1F5B8 { background-position: -18400px 0px; }
-.emoji-1F5B9 { background-position: -18420px 0px; }
-.emoji-1F5BC { background-position: -18440px 0px; }
-.emoji-1F5BD { background-position: -18460px 0px; }
-.emoji-1F5BE { background-position: -18480px 0px; }
-.emoji-1F5C0 { background-position: -18500px 0px; }
-.emoji-1F5C1 { background-position: -18520px 0px; }
-.emoji-1F5C2 { background-position: -18540px 0px; }
-.emoji-1F5C3 { background-position: -18560px 0px; }
-.emoji-1F5C4 { background-position: -18580px 0px; }
-.emoji-1F5C6 { background-position: -18600px 0px; }
-.emoji-1F5C7 { background-position: -18620px 0px; }
-.emoji-1F5C9 { background-position: -18640px 0px; }
-.emoji-1F5CA { background-position: -18660px 0px; }
-.emoji-1F5CE { background-position: -18680px 0px; }
-.emoji-1F5CF { background-position: -18700px 0px; }
-.emoji-1F5D0 { background-position: -18720px 0px; }
-.emoji-1F5D1 { background-position: -18740px 0px; }
-.emoji-1F5D2 { background-position: -18760px 0px; }
-.emoji-1F5D3 { background-position: -18780px 0px; }
-.emoji-1F5D4 { background-position: -18800px 0px; }
-.emoji-1F5D8 { background-position: -18820px 0px; }
-.emoji-1F5D9 { background-position: -18840px 0px; }
-.emoji-1F5DC { background-position: -18860px 0px; }
-.emoji-1F5DD { background-position: -18880px 0px; }
-.emoji-1F5DE { background-position: -18900px 0px; }
-.emoji-1F5E0 { background-position: -18920px 0px; }
-.emoji-1F5E1 { background-position: -18940px 0px; }
-.emoji-1F5E2 { background-position: -18960px 0px; }
-.emoji-1F5E3 { background-position: -18980px 0px; }
-.emoji-1F5E8 { background-position: -19000px 0px; }
-.emoji-1F5E9 { background-position: -19020px 0px; }
-.emoji-1F5EA { background-position: -19040px 0px; }
-.emoji-1F5EB { background-position: -19060px 0px; }
-.emoji-1F5EC { background-position: -19080px 0px; }
-.emoji-1F5ED { background-position: -19100px 0px; }
-.emoji-1F5EE { background-position: -19120px 0px; }
-.emoji-1F5EF { background-position: -19140px 0px; }
-.emoji-1F5F0 { background-position: -19160px 0px; }
-.emoji-1F5F1 { background-position: -19180px 0px; }
-.emoji-1F5F2 { background-position: -19200px 0px; }
-.emoji-1F5F3 { background-position: -19220px 0px; }
-.emoji-1F5F4 { background-position: -19240px 0px; }
-.emoji-1F5F5 { background-position: -19260px 0px; }
-.emoji-1F5F8 { background-position: -19280px 0px; }
-.emoji-1F5F9 { background-position: -19300px 0px; }
-.emoji-1F5FA { background-position: -19320px 0px; }
-.emoji-1F5FB { background-position: -19340px 0px; }
-.emoji-1F5FC { background-position: -19360px 0px; }
-.emoji-1F5FD { background-position: -19380px 0px; }
-.emoji-1F5FE { background-position: -19400px 0px; }
-.emoji-1F5FF { background-position: -19420px 0px; }
-.emoji-1F600 { background-position: -19440px 0px; }
-.emoji-1F601 { background-position: -19460px 0px; }
-.emoji-1F602 { background-position: -19480px 0px; }
-.emoji-1F603 { background-position: -19500px 0px; }
-.emoji-1F604 { background-position: -19520px 0px; }
-.emoji-1F605 { background-position: -19540px 0px; }
-.emoji-1F606 { background-position: -19560px 0px; }
-.emoji-1F607 { background-position: -19580px 0px; }
-.emoji-1F608 { background-position: -19600px 0px; }
-.emoji-1F609 { background-position: -19620px 0px; }
-.emoji-1F60A { background-position: -19640px 0px; }
-.emoji-1F60B { background-position: -19660px 0px; }
-.emoji-1F60C { background-position: -19680px 0px; }
-.emoji-1F60D { background-position: -19700px 0px; }
-.emoji-1F60E { background-position: -19720px 0px; }
-.emoji-1F60F { background-position: -19740px 0px; }
-.emoji-1F610 { background-position: -19760px 0px; }
-.emoji-1F611 { background-position: -19780px 0px; }
-.emoji-1F612 { background-position: -19800px 0px; }
-.emoji-1F613 { background-position: -19820px 0px; }
-.emoji-1F614 { background-position: -19840px 0px; }
-.emoji-1F615 { background-position: -19860px 0px; }
-.emoji-1F616 { background-position: -19880px 0px; }
-.emoji-1F617 { background-position: -19900px 0px; }
-.emoji-1F618 { background-position: -19920px 0px; }
-.emoji-1F619 { background-position: -19940px 0px; }
-.emoji-1F61A { background-position: -19960px 0px; }
-.emoji-1F61B { background-position: -19980px 0px; }
-.emoji-1F61C { background-position: -20000px 0px; }
-.emoji-1F61D { background-position: -20020px 0px; }
-.emoji-1F61E { background-position: -20040px 0px; }
-.emoji-1F61F { background-position: -20060px 0px; }
-.emoji-1F620 { background-position: -20080px 0px; }
-.emoji-1F621 { background-position: -20100px 0px; }
-.emoji-1F622 { background-position: -20120px 0px; }
-.emoji-1F623 { background-position: -20140px 0px; }
-.emoji-1F624 { background-position: -20160px 0px; }
-.emoji-1F625 { background-position: -20180px 0px; }
-.emoji-1F626 { background-position: -20200px 0px; }
-.emoji-1F627 { background-position: -20220px 0px; }
-.emoji-1F628 { background-position: -20240px 0px; }
-.emoji-1F629 { background-position: -20260px 0px; }
-.emoji-1F62A { background-position: -20280px 0px; }
-.emoji-1F62B { background-position: -20300px 0px; }
-.emoji-1F62C { background-position: -20320px 0px; }
-.emoji-1F62D { background-position: -20340px 0px; }
-.emoji-1F62E { background-position: -20360px 0px; }
-.emoji-1F62F { background-position: -20380px 0px; }
-.emoji-1F630 { background-position: -20400px 0px; }
-.emoji-1F631 { background-position: -20420px 0px; }
-.emoji-1F632 { background-position: -20440px 0px; }
-.emoji-1F633 { background-position: -20460px 0px; }
-.emoji-1F634 { background-position: -20480px 0px; }
-.emoji-1F635 { background-position: -20500px 0px; }
-.emoji-1F636 { background-position: -20520px 0px; }
-.emoji-1F637 { background-position: -20540px 0px; }
-.emoji-1F638 { background-position: -20560px 0px; }
-.emoji-1F639 { background-position: -20580px 0px; }
-.emoji-1F63A { background-position: -20600px 0px; }
-.emoji-1F63B { background-position: -20620px 0px; }
-.emoji-1F63C { background-position: -20640px 0px; }
-.emoji-1F63D { background-position: -20660px 0px; }
-.emoji-1F63E { background-position: -20680px 0px; }
-.emoji-1F63F { background-position: -20700px 0px; }
-.emoji-1F640 { background-position: -20720px 0px; }
-.emoji-1F641 { background-position: -20740px 0px; }
-.emoji-1F642 { background-position: -20760px 0px; }
-.emoji-1F645 { background-position: -20780px 0px; }
-.emoji-1F646 { background-position: -20800px 0px; }
-.emoji-1F647 { background-position: -20820px 0px; }
-.emoji-1F648 { background-position: -20840px 0px; }
-.emoji-1F649 { background-position: -20860px 0px; }
-.emoji-1F64A { background-position: -20880px 0px; }
-.emoji-1F64B { background-position: -20900px 0px; }
-.emoji-1F64C { background-position: -20920px 0px; }
-.emoji-1F64D { background-position: -20940px 0px; }
-.emoji-1F64E { background-position: -20960px 0px; }
-.emoji-1F64F { background-position: -20980px 0px; }
-.emoji-1F680 { background-position: -21000px 0px; }
-.emoji-1F681 { background-position: -21020px 0px; }
-.emoji-1F682 { background-position: -21040px 0px; }
-.emoji-1F683 { background-position: -21060px 0px; }
-.emoji-1F684 { background-position: -21080px 0px; }
-.emoji-1F685 { background-position: -21100px 0px; }
-.emoji-1F686 { background-position: -21120px 0px; }
-.emoji-1F687 { background-position: -21140px 0px; }
-.emoji-1F688 { background-position: -21160px 0px; }
-.emoji-1F689 { background-position: -21180px 0px; }
-.emoji-1F68A { background-position: -21200px 0px; }
-.emoji-1F68B { background-position: -21220px 0px; }
-.emoji-1F68C { background-position: -21240px 0px; }
-.emoji-1F68D { background-position: -21260px 0px; }
-.emoji-1F68E { background-position: -21280px 0px; }
-.emoji-1F68F { background-position: -21300px 0px; }
-.emoji-1F690 { background-position: -21320px 0px; }
-.emoji-1F691 { background-position: -21340px 0px; }
-.emoji-1F692 { background-position: -21360px 0px; }
-.emoji-1F693 { background-position: -21380px 0px; }
-.emoji-1F694 { background-position: -21400px 0px; }
-.emoji-1F695 { background-position: -21420px 0px; }
-.emoji-1F696 { background-position: -21440px 0px; }
-.emoji-1F697 { background-position: -21460px 0px; }
-.emoji-1F698 { background-position: -21480px 0px; }
-.emoji-1F699 { background-position: -21500px 0px; }
-.emoji-1F69A { background-position: -21520px 0px; }
-.emoji-1F69B { background-position: -21540px 0px; }
-.emoji-1F69C { background-position: -21560px 0px; }
-.emoji-1F69D { background-position: -21580px 0px; }
-.emoji-1F69E { background-position: -21600px 0px; }
-.emoji-1F69F { background-position: -21620px 0px; }
-.emoji-1F6A0 { background-position: -21640px 0px; }
-.emoji-1F6A1 { background-position: -21660px 0px; }
-.emoji-1F6A2 { background-position: -21680px 0px; }
-.emoji-1F6A3 { background-position: -21700px 0px; }
-.emoji-1F6A4 { background-position: -21720px 0px; }
-.emoji-1F6A5 { background-position: -21740px 0px; }
-.emoji-1F6A6 { background-position: -21760px 0px; }
-.emoji-1F6A7 { background-position: -21780px 0px; }
-.emoji-1F6A8 { background-position: -21800px 0px; }
-.emoji-1F6A9 { background-position: -21820px 0px; }
-.emoji-1F6AA { background-position: -21840px 0px; }
-.emoji-1F6AB { background-position: -21860px 0px; }
-.emoji-1F6AC { background-position: -21880px 0px; }
-.emoji-1F6AD { background-position: -21900px 0px; }
-.emoji-1F6AE { background-position: -21920px 0px; }
-.emoji-1F6AF { background-position: -21940px 0px; }
-.emoji-1F6B0 { background-position: -21960px 0px; }
-.emoji-1F6B1 { background-position: -21980px 0px; }
-.emoji-1F6B2 { background-position: -22000px 0px; }
-.emoji-1F6B3 { background-position: -22020px 0px; }
-.emoji-1F6B4 { background-position: -22040px 0px; }
-.emoji-1F6B5 { background-position: -22060px 0px; }
-.emoji-1F6B6 { background-position: -22080px 0px; }
-.emoji-1F6B7 { background-position: -22100px 0px; }
-.emoji-1F6B8 { background-position: -22120px 0px; }
-.emoji-1F6B9 { background-position: -22140px 0px; }
-.emoji-1F6BA { background-position: -22160px 0px; }
-.emoji-1F6BB { background-position: -22180px 0px; }
-.emoji-1F6BC { background-position: -22200px 0px; }
-.emoji-1F6BD { background-position: -22220px 0px; }
-.emoji-1F6BE { background-position: -22240px 0px; }
-.emoji-1F6BF { background-position: -22260px 0px; }
-.emoji-1F6C0 { background-position: -22280px 0px; }
-.emoji-1F6C1 { background-position: -22300px 0px; }
-.emoji-1F6C2 { background-position: -22320px 0px; }
-.emoji-1F6C3 { background-position: -22340px 0px; }
-.emoji-1F6C4 { background-position: -22360px 0px; }
-.emoji-1F6C5 { background-position: -22380px 0px; }
-.emoji-1F6C6 { background-position: -22400px 0px; }
-.emoji-1F6C7 { background-position: -22420px 0px; }
-.emoji-1F6C8 { background-position: -22440px 0px; }
-.emoji-1F6C9 { background-position: -22460px 0px; }
-.emoji-1F6CA { background-position: -22480px 0px; }
-.emoji-1F6CB { background-position: -22500px 0px; }
-.emoji-1F6CC { background-position: -22520px 0px; }
-.emoji-1F6CD { background-position: -22540px 0px; }
-.emoji-1F6CE { background-position: -22560px 0px; }
-.emoji-1F6CF { background-position: -22580px 0px; }
-.emoji-1F6E0 { background-position: -22600px 0px; }
-.emoji-1F6E1 { background-position: -22620px 0px; }
-.emoji-1F6E2 { background-position: -22640px 0px; }
-.emoji-1F6E3 { background-position: -22660px 0px; }
-.emoji-1F6E4 { background-position: -22680px 0px; }
-.emoji-1F6E5 { background-position: -22700px 0px; }
-.emoji-1F6E6 { background-position: -22720px 0px; }
-.emoji-1F6E7 { background-position: -22740px 0px; }
-.emoji-1F6E8 { background-position: -22760px 0px; }
-.emoji-1F6E9 { background-position: -22780px 0px; }
-.emoji-1F6EA { background-position: -22800px 0px; }
-.emoji-1F6EB { background-position: -22820px 0px; }
-.emoji-1F6EC { background-position: -22840px 0px; }
-.emoji-1F6F0 { background-position: -22860px 0px; }
-.emoji-1F6F1 { background-position: -22880px 0px; }
-.emoji-1F6F2 { background-position: -22900px 0px; }
-.emoji-1F6F3 { background-position: -22920px 0px; }
-.emoji-203C { background-position: -22940px 0px; }
-.emoji-2049 { background-position: -22960px 0px; }
-.emoji-2122 { background-position: -22980px 0px; }
-.emoji-2139 { background-position: -23000px 0px; }
-.emoji-2194 { background-position: -23020px 0px; }
-.emoji-2195 { background-position: -23040px 0px; }
-.emoji-2196 { background-position: -23060px 0px; }
-.emoji-2197 { background-position: -23080px 0px; }
-.emoji-2198 { background-position: -23100px 0px; }
-.emoji-2199 { background-position: -23120px 0px; }
-.emoji-21A9 { background-position: -23140px 0px; }
-.emoji-21AA { background-position: -23160px 0px; }
-.emoji-231A { background-position: -23180px 0px; }
-.emoji-231B { background-position: -23200px 0px; }
-.emoji-23E9 { background-position: -23220px 0px; }
-.emoji-23EA { background-position: -23240px 0px; }
-.emoji-23EB { background-position: -23260px 0px; }
-.emoji-23EC { background-position: -23280px 0px; }
-.emoji-23F0 { background-position: -23300px 0px; }
-.emoji-23F3 { background-position: -23320px 0px; }
-.emoji-24C2 { background-position: -23340px 0px; }
-.emoji-25AA { background-position: -23360px 0px; }
-.emoji-25AB { background-position: -23380px 0px; }
-.emoji-25B6 { background-position: -23400px 0px; }
-.emoji-25C0 { background-position: -23420px 0px; }
-.emoji-25FB { background-position: -23440px 0px; }
-.emoji-25FC { background-position: -23460px 0px; }
-.emoji-25FD { background-position: -23480px 0px; }
-.emoji-25FE { background-position: -23500px 0px; }
-.emoji-2600 { background-position: -23520px 0px; }
-.emoji-2601 { background-position: -23540px 0px; }
-.emoji-260E { background-position: -23560px 0px; }
-.emoji-2611 { background-position: -23580px 0px; }
-.emoji-2614 { background-position: -23600px 0px; }
-.emoji-2615 { background-position: -23620px 0px; }
-.emoji-261D { background-position: -23640px 0px; }
-.emoji-263A { background-position: -23660px 0px; }
-.emoji-2648 { background-position: -23680px 0px; }
-.emoji-2649 { background-position: -23700px 0px; }
-.emoji-264A { background-position: -23720px 0px; }
-.emoji-264B { background-position: -23740px 0px; }
-.emoji-264C { background-position: -23760px 0px; }
-.emoji-264D { background-position: -23780px 0px; }
-.emoji-264E { background-position: -23800px 0px; }
-.emoji-264F { background-position: -23820px 0px; }
-.emoji-2650 { background-position: -23840px 0px; }
-.emoji-2651 { background-position: -23860px 0px; }
-.emoji-2652 { background-position: -23880px 0px; }
-.emoji-2653 { background-position: -23900px 0px; }
-.emoji-2660 { background-position: -23920px 0px; }
-.emoji-2663 { background-position: -23940px 0px; }
-.emoji-2665 { background-position: -23960px 0px; }
-.emoji-2666 { background-position: -23980px 0px; }
-.emoji-2668 { background-position: -24000px 0px; }
-.emoji-267B { background-position: -24020px 0px; }
-.emoji-267F { background-position: -24040px 0px; }
-.emoji-2693 { background-position: -24060px 0px; }
-.emoji-26A0 { background-position: -24080px 0px; }
-.emoji-26A1 { background-position: -24100px 0px; }
-.emoji-26AA { background-position: -24120px 0px; }
-.emoji-26AB { background-position: -24140px 0px; }
-.emoji-26BD { background-position: -24160px 0px; }
-.emoji-26BE { background-position: -24180px 0px; }
-.emoji-26C4 { background-position: -24200px 0px; }
-.emoji-26C5 { background-position: -24220px 0px; }
-.emoji-26CE { background-position: -24240px 0px; }
-.emoji-26D4 { background-position: -24260px 0px; }
-.emoji-26EA { background-position: -24280px 0px; }
-.emoji-26F2 { background-position: -24300px 0px; }
-.emoji-26F3 { background-position: -24320px 0px; }
-.emoji-26F5 { background-position: -24340px 0px; }
-.emoji-26FA { background-position: -24360px 0px; }
-.emoji-26FD { background-position: -24380px 0px; }
-.emoji-2702 { background-position: -24400px 0px; }
-.emoji-2705 { background-position: -24420px 0px; }
-.emoji-2708 { background-position: -24440px 0px; }
-.emoji-2709 { background-position: -24460px 0px; }
-.emoji-270A { background-position: -24480px 0px; }
-.emoji-270B { background-position: -24500px 0px; }
-.emoji-270C { background-position: -24520px 0px; }
-.emoji-270F { background-position: -24540px 0px; }
-.emoji-2712 { background-position: -24560px 0px; }
-.emoji-2714 { background-position: -24580px 0px; }
-.emoji-2716 { background-position: -24600px 0px; }
-.emoji-2728 { background-position: -24620px 0px; }
-.emoji-2733 { background-position: -24640px 0px; }
-.emoji-2734 { background-position: -24660px 0px; }
-.emoji-2744 { background-position: -24680px 0px; }
-.emoji-2747 { background-position: -24700px 0px; }
-.emoji-274C { background-position: -24720px 0px; }
-.emoji-274E { background-position: -24740px 0px; }
-.emoji-2753 { background-position: -24760px 0px; }
-.emoji-2754 { background-position: -24780px 0px; }
-.emoji-2755 { background-position: -24800px 0px; }
-.emoji-2757 { background-position: -24820px 0px; }
-.emoji-2764 { background-position: -24840px 0px; }
-.emoji-2795 { background-position: -24860px 0px; }
-.emoji-2796 { background-position: -24880px 0px; }
-.emoji-2797 { background-position: -24900px 0px; }
-.emoji-27A1 { background-position: -24920px 0px; }
-.emoji-27B0 { background-position: -24940px 0px; }
-.emoji-27BF { background-position: -24960px 0px; }
-.emoji-2934 { background-position: -24980px 0px; }
-.emoji-2935 { background-position: -25000px 0px; }
-.emoji-2B05 { background-position: -25020px 0px; }
-.emoji-2B06 { background-position: -25040px 0px; }
-.emoji-2B07 { background-position: -25060px 0px; }
-.emoji-2B1B { background-position: -25080px 0px; }
-.emoji-2B1C { background-position: -25100px 0px; }
-.emoji-2B50 { background-position: -25120px 0px; }
-.emoji-2B55 { background-position: -25140px 0px; }
-.emoji-3030 { background-position: -25160px 0px; }
-.emoji-303D { background-position: -25180px 0px; }
-.emoji-3297 { background-position: -25200px 0px; }
-.emoji-3299 { background-position: -25220px 0px; } \ No newline at end of file
+ @media only screen and (-webkit-min-device-pixel-ratio: 2),
+ only screen and (min--moz-device-pixel-ratio: 2),
+ only screen and (-o-min-device-pixel-ratio: 2/1),
+ only screen and (min-device-pixel-ratio: 2),
+ only screen and (min-resolution: 192dpi),
+ only screen and (min-resolution: 2dppx) {
+ background-image: image-url('emoji@2x.png');
+ background-size: 840px 820px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 282aaf2219b..b39a9abf40f 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,9 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px);
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
+ padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
border-bottom: 1px solid $table-border-color;
color: #7f8fa4;
@@ -18,7 +16,7 @@
.event-title,
.event-item-timestamp {
- line-height: 44px;
+ line-height: 40px;
}
}
@@ -27,7 +25,7 @@
}
.avatar {
- margin-left: -($gl-avatar-size + 15px);
+ margin-left: -($gl-avatar-size + $gl-padding-top);
}
.event-title {
@@ -43,7 +41,6 @@
margin-right: 174px;
.event-note {
- margin-top: 5px;
word-wrap: break-word;
.md {
@@ -66,7 +63,7 @@
.note-image-attach {
margin-top: 4px;
- margin-left: 0px;
+ margin-left: 0;
max-width: 200px;
float: none;
}
@@ -86,10 +83,10 @@
.event_icon {
position: relative;
float: right;
- border: 1px solid #EEE;
+ border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
- background: #F9F9F9;
+ background: #f9f9f9;
margin-left: 10px;
top: -6px;
img {
@@ -97,11 +94,9 @@
}
}
- &:last-child { border:none }
+ &:last-child { border: none }
.event_commits {
- margin-top: 9px;
-
li {
&.commit {
background: transparent;
@@ -138,11 +133,12 @@
*/
.event-last-push {
overflow: auto;
+ width: 100%;
.event-last-push-text {
@include str-truncated(100%);
padding: 5px 0;
font-size: 13px;
- float:left;
+ float: left;
margin-right: -150px;
padding-right: 150px;
line-height: 20px;
@@ -164,7 +160,7 @@
.event-body {
margin: 0;
- border-left: 2px solid #DDD;
+ border-left: 2px solid #ddd;
padding-left: 10px;
}
diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss
index da06fe9954e..9b92128624c 100644
--- a/app/assets/stylesheets/pages/explore.scss
+++ b/app/assets/stylesheets/pages/explore.scss
@@ -6,11 +6,3 @@
font-size: 30px;
}
}
-
-.explore-trending-block {
- .lead {
- line-height: 32px;
- font-size: 18px;
- margin-top: 10px;
- }
-}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index c3b10d144e1..4e5c4ed84b6 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -6,11 +6,11 @@
font-size: 14px;
padding: 5px;
border-bottom: 1px solid $border-color;
- background: #EEE;
+ background: #eee;
}
.network-graph {
- background: #FFF;
+ background: #fff;
height: 500px;
overflow-y: scroll;
overflow-x: hidden;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 263993f59a5..ec6c099df5b 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -1,5 +1,15 @@
.member-search-form {
float: left;
+
+ input[type='search'] {
+ width: 225px;
+ vertical-align: bottom;
+
+ @media (max-width: $screen-xs-max) {
+ width: 100px;
+ vertical-align: bottom;
+ }
+ }
}
.milestone-row {
@@ -11,3 +21,21 @@
height: 42px;
}
}
+
+.group-row {
+ &.no-description {
+ .group-name {
+ line-height: 44px;
+ }
+ }
+
+ .stats {
+ float: right;
+ line-height: 44px;
+ color: $gl-gray;
+
+ span {
+ margin-right: 15px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 3df4bb84bd2..6a99cd9cb94 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,6 +1,6 @@
i.icon-gitorious {
display: inline-block;
- background-position: 0px 0px;
+ background-position: 0 0;
background-size: contain;
background-repeat: no-repeat;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 9da273a0b6b..6f93299404c 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -20,25 +20,17 @@
position: fixed;
top: 70px;
margin-right: 35px;
- }
- }
-}
-
-.project-issuable-filter {
- .controls {
- float: right;
- margin-top: 7px;
- }
- .center-top-menu {
- text-align: left;
+ &.no-affix {
+ position: relative;
+ top: 0;
+ }
+ }
}
}
.issuable-details {
section {
- border-right: 1px solid $border-white-light;
-
.issuable-discussion {
margin-right: 1px;
}
@@ -67,12 +59,42 @@
.issuable-sidebar {
.block {
@include clearfix;
- padding: $gl-padding 0;
- border-bottom: 1px solid #F0F0F0;
+ padding: $gl-padding 0;
+ border-bottom: 1px solid $border-gray-light;
+ // This prevents the mess when resizing the sidebar
+ // of elements repositioning themselves..
+ width: $gutter_inner_width;
+ // --
+
+ &:first-child {
+ padding-top: 5px;
+ }
&:last-child {
border: none;
}
+
+ span {
+ margin-top: 7px;
+ display: inline-block;
+ }
+
+ .select2-container span {
+ margin-top: 0;
+ }
+
+ .issuable-count {
+
+ }
+
+ .gutter-toggle {
+ margin-left: 20px;
+ padding-left: 10px;
+
+ &:hover {
+ color: $gray-darkest;
+ }
+ }
}
.title {
@@ -94,11 +116,24 @@
}
.cross-project-reference {
- font-weight: bold;
color: $gl-link-color;
+ span {
+ white-space: nowrap;
+ width: 85%;
+ overflow: hidden;
+ position: relative;
+ display: inline-block;
+ text-overflow: ellipsis;
+ }
+
+ cite {
+ font-style: normal;
+ }
+
button {
float: right;
+ padding: 3px 5px;
}
}
@@ -115,3 +150,123 @@
margin-right: 2px;
}
}
+
+.right-sidebar {
+ position: fixed;
+ top: 58px;
+ bottom: 0;
+ right: 0;
+ transition: width .3s;
+ background: $gray-light;
+ padding: 10px 20px;
+
+ &.right-sidebar-expanded {
+ width: $gutter_width;
+
+ hr {
+ display: none;
+ }
+
+ .sidebar-collapsed-icon {
+ display: none;
+ }
+
+ .gutter-toggle {
+ border-left: 1px solid $border-gray-light;
+ }
+ }
+
+ .subscribe-button {
+ span {
+ margin-top: 0;
+ }
+ }
+
+ &.right-sidebar-collapsed {
+ /* Extra small devices (phones, less than 768px) */
+ display: none;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ display: block
+ }
+
+ 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;
+ border-bottom: none;
+ overflow: hidden;
+ }
+
+ .hide-collapsed {
+ display: none;
+ }
+
+ .gutter-toggle {
+ margin-left: -36px;
+ }
+
+ .sidebar-collapsed-icon {
+ display: block;
+ width: 100%;
+ text-align: center;
+ padding-bottom: 10px;
+ color: #999;
+
+ span {
+ display: block;
+ margin-top: 0;
+ }
+
+ .btn-clipboard {
+ border: none;
+
+ &:hover {
+ background: transparent;
+ }
+
+ i {
+ color: #999;
+ }
+ }
+ }
+ }
+
+ .btn {
+ background: $gray-normal;
+ border: 1px solid $border-gray-normal;
+ &:hover {
+ background: $gray-dark;
+ border: 1px solid $border-gray-dark;
+ }
+ }
+}
+
+.btn-default.gutter-toggle {
+ margin-top: 4px;
+}
+
+.detail-page-description {
+ small {
+ color: $gray-darkest;
+ }
+}
+
+.edited-text {
+ color: $gray-darkest;
+
+ .author_link {
+ color: $gray-darkest;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index a02a3a72e79..7ac4bc468d6 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -4,13 +4,7 @@
position: relative;
.issue-title {
- margin-bottom: 5px;
- font-size: $list-font-size;
- font-weight: bold;
- }
-
- .issue-info {
- color: $gl-gray;
+ margin-bottom: 2px;
}
.issue-check {
@@ -49,18 +43,13 @@
.issue-search-form {
margin: 0;
height: 24px;
-
- .issue_search {
- border: 1px solid #DDD !important;
- background-color: #f4f4f4;
- }
}
form.edit-issue {
margin: 0;
}
-.merge-requests-title {
+.merge-requests-title, .related-branches-title {
font-size: 16px;
font-weight: 600;
}
@@ -70,10 +59,6 @@ form.edit-issue {
width: 3em;
}
-.merge-request-info {
- padding-left: 5px;
-}
-
.merge-request-status {
color: $gl-gray;
font-size: 15px;
@@ -83,18 +68,18 @@ form.edit-issue {
.merge-request,
.issue {
&.today {
- background: #EFE;
- border-color: #CEC;
+ background: #efe;
+ border-color: #cec;
}
&.closed {
- background: #F9F9F9;
- border-color: #E5E5E5;
+ background: #f9f9f9;
+ border-color: #e5e5e5;
}
&.merged {
- background: #F9F9F9;
- border-color: #E5E5E5;
+ background: #f9f9f9;
+ border-color: #e5e5e5;
}
}
@@ -114,18 +99,17 @@ form.edit-issue {
.btn {
width: 100%;
- margin-top: -1px;
&:first-child:not(:last-child) {
- border-radius: 4px 4px 0 0;
+
}
&:not(:first-child):not(:last-child) {
- border-radius: 0;
+ margin-top: 10px;
}
&:last-child:not(:first-child) {
- border-radius: 0 0 4px 4px;
+ margin-top: 10px;
}
}
}
@@ -144,3 +128,16 @@ form.edit-issue {
.issue-form .select2-container {
width: 250px !important;
}
+
+.issue-closed-by-widget {
+ color: $secondary-text;
+ 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 d1590e42fcb..61ee34b695e 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -7,9 +7,31 @@
display: inline-block;
margin-right: 10px;
}
+
+ &.suggest-colors-dropdown {
+ margin-bottom: 5px;
+
+ a {
+ @include border-radius(0);
+ width: 36.7px;
+ margin-right: 0;
+ margin-bottom: -5px;
+ }
+ }
+}
+
+.dropdown-label-color-preview {
+ display: none;
+ margin-top: 5px;
+ width: 100%;
+ height: 25px;
+
+ &.is-active {
+ display: block;
+ }
}
-.manage-labels-list {
+.label-row {
.label {
padding: 9px;
font-size: 14px;
@@ -19,3 +41,7 @@
.color-label {
padding: 3px 4px;
}
+
+.label-subscription {
+ display: inline-block;
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index f9c6f1b39f9..bc41f7d306f 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -8,6 +8,10 @@
max-width: none;
}
+ .flash-container {
+ margin-bottom: $gl-padding;
+ }
+
.brand-holder {
font-size: 18px;
line-height: 1.5;
@@ -24,7 +28,7 @@
img {
max-width: 100%;
- margin-bottom: 30px;
+ margin-bottom: 30px;
}
a {
@@ -35,7 +39,7 @@
.login-box{
background: #fafafa;
border-radius: 10px;
- box-shadow: 0 0px 2px #CCC;
+ box-shadow: 0 0 2px #ccc;
padding: 15px;
.login-heading h3 {
@@ -70,7 +74,7 @@
&.top {
@include border-radius(5px 5px 0 0);
- margin-bottom: 0px;
+ margin-bottom: 0;
}
&.bottom {
@@ -81,12 +85,12 @@
&.middle {
border-top: 0;
- margin-bottom:0px;
+ margin-bottom: 0;
@include border-radius(0);
}
&:active, &:focus {
- background-color: #FFF;
+ background-color: #fff;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 82effde0bf3..cee5c47cfb2 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,9 +3,9 @@
*
*/
.mr-state-widget {
- background: #F7F8FA;
+ background: $background-color;
color: $gl-gray;
- border: 1px solid #dce0e6;
+ border: 1px solid $border-color;
@include border-radius(2px);
form {
@@ -113,7 +113,7 @@
}
.mr-widget-footer {
- border-top: 1px solid #EEE;
+ border-top: 1px solid #eee;
}
.ci-coverage {
@@ -148,15 +148,8 @@
position: relative;
.merge-request-title {
- margin-bottom: 5px;
- font-size: $list-font-size;
- font-weight: bold;
+ margin-bottom: 2px;
}
-
- .merge-request-info {
- color: $gl-gray;
- }
-
}
.merge-request-labels {
@@ -201,3 +194,39 @@
.mr-source-target {
line-height: 31px;
}
+
+.disabled-comment-area {
+ padding: 16px 0;
+
+ .disabled-profile {
+ width: 40px;
+ height: 40px;
+ background: $border-gray-dark;
+ border-radius: 20px;
+ display: inline-block;
+ margin-right: 10px;
+ }
+
+ .disabled-comment {
+ background: $gray-light;
+ display: inline-block;
+ vertical-align: top;
+ height: 200px;
+ border-radius: 4px;
+ border: 1px solid $border-gray-normal;
+ padding-top: 90px;
+ text-align: center;
+ right: 20px;
+ position: absolute;
+ left: 70px;
+ margin-bottom: 20px;
+
+ span {
+ color: #b2b2b2;
+
+ a {
+ color: $md-link-color;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index e80dc9e84a1..d0e72a4422c 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -11,3 +11,56 @@ li.milestone {
height: 6px;
}
}
+
+.milestone-content {
+ .issues-count {
+ margin-right: 17px;
+ float: right;
+ width: 105px;
+ }
+
+ .issuable-row {
+ .color-label {
+ border-radius: 2px;
+ padding: 3px !important;
+ margin-right: 7px;
+ }
+
+ // Issue title
+ span a {
+ color: rgba(0,0,0,0.64);
+ }
+ }
+}
+
+.milestone-summary {
+ margin-bottom: 25px;
+
+ .milestone-stat {
+ margin-right: 10px;
+ }
+
+ .remaining-days {
+ color: $orange-light;
+ }
+}
+
+.issues-sortable-list, .merge_requests-sortable-list {
+ .issuable-detail {
+ display: block;
+ margin-top: 7px;
+
+ .issuable-number {
+ color: rgba(0,0,0,0.44);
+ margin-right: 5px;
+ }
+ .avatar {
+ float: none;
+ }
+ }
+}
+
+.milestone-detail {
+ border-bottom: 1px solid $border-color;
+ padding: 20px 0;
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index d86259f93fb..61783ec46aa 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -10,18 +10,6 @@
margin: 10px $gl-padding;
}
.diff-file .diff-content {
- tr.line_holder:hover {
- &> td.line_content {
- background: $hover !important;
- border-color: darken($hover, 10%) !important;
- }
- &> td.new_line,
- &> td.old_line {
- background: darken($hover, 4%) !important;
- border-color: darken($hover, 10%) !important;
- }
- }
-
tr.line_holder:hover > td .line_note_link {
opacity: 1.0;
filter: alpha(opacity=100);
@@ -159,6 +147,7 @@
.edit_note {
.markdown-area {
min-height: 140px;
+ max-height: 500px;
}
.note-form-actions {
background: transparent;
@@ -167,7 +156,7 @@
.comment-hints {
color: #999;
- background: #FFF;
+ background: #fff;
padding: 7px;
margin-top: -7px;
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 72b0ed29a69..d408853cc80 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,22 +3,34 @@
*/
@-webkit-keyframes targe3-note {
- from { background:#fffff0; }
- 50% { background:#ffffd3; }
- to { background:#fffff0; }
+ from { background: #fffff0; }
+ 50% { background: #ffffd3; }
+ to { background: #fffff0; }
}
ul.notes {
display: block;
list-style: none;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
+
+ .timeline-icon {
+ float: left;
+ }
+
+ .timeline-content {
+ margin-left: 55px;
+ }
+
+ .note_created_ago, .note-updated-at {
+ white-space: nowrap;
+ }
.system-note {
font-size: 14px;
padding-top: 10px;
padding-bottom: 10px;
- background: #FDFDFD;
+ background: #fdfdfd;
.timeline-icon {
.avatar {
@@ -81,12 +93,12 @@ ul.notes {
.discussion {
overflow: hidden;
display: block;
- position:relative;
+ position: relative;
}
.note {
display: block;
- position:relative;
+ position: relative;
.note-body {
overflow: auto;
@@ -96,6 +108,13 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
+ // On diffs code should wrap nicely and not overflow
+ pre {
+ code {
+ white-space: pre-wrap;
+ }
+ }
+
// Reset ul style types since we're nested inside a ul already
& > ul {
list-style-type: disc;
@@ -117,7 +136,7 @@ ul.notes {
hr {
// Darken 'whitesmoke' a bit to make it more visible in note bodies
- border-color: darken(#F5F5F5, 8%);
+ border-color: darken(#f5f5f5, 8%);
margin: 10px 0;
}
}
@@ -151,9 +170,11 @@ ul.notes {
border-left: none;
&.notes_line {
+ vertical-align: middle;
text-align: center;
padding: 10px 0;
- background: #FFF;
+ background: #fff;
+ color: $text-color;
}
&.notes_line2 {
text-align: center;
@@ -218,7 +239,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
@include border-radius(40px);
- background: #FFF;
+ background: #fff;
padding: 4px;
font-size: 16px;
color: $gl-link-color;
@@ -235,18 +256,15 @@ ul.notes {
&:hover {
background: $gl-info;
- color: #FFF;
+ color: #fff;
@include show-add-diff-note;
}
}
// "show" the icon also if we just hover somewhere over the line
&:hover > td {
- background: $hover !important;
-
.add-diff-note {
@include show-add-diff-note;
}
}
}
-
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index cc273f55222..94fbbef3c77 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -1,16 +1,18 @@
-.global-notifications-form .level-title {
- font-size: 15px;
- color: #333;
- font-weight: bold;
+.notification-list-item {
+ line-height: 34px;
}
-.notification-icon-holder {
- width: 20px;
- float: left;
+.notification {
+ position: relative;
+ top: 1px;
+
+ > .fa {
+ font-size: 18px;
+ }
}
.ns-part {
- color: $gl-primary;
+ color: $gl-text-green;
}
.ns-watch {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 95fc26a608a..260179074cf 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -1,16 +1,27 @@
-.account-page {
- fieldset {
- margin-bottom: 15px;
- padding-bottom: 15px;
- }
-}
-
.profile-avatar-form-option {
hr {
margin: 10px 0;
}
}
+.avatar-image {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ margin-bottom: 0;
+ }
+}
+
+.avatar-file-name {
+ position: relative;
+ top: 2px;
+ display: inline-block;
+}
+
+.account-btn-link,
+.profile-settings-sidebar a {
+ color: $md-link-color;
+}
+
.oauth-buttons {
.btn-group {
margin-right: 10px;
@@ -19,7 +30,7 @@
.btn {
line-height: 40px;
height: 42px;
- padding: 0px 12px;
+ padding: 0 12px;
img {
width: 32px;
@@ -42,6 +53,18 @@
}
}
+.account-well {
+ padding: 10px 10px;
+ background-color: $help-well-bg;
+ border: 1px solid $help-well-border;
+ border-radius: $border-radius-base;
+
+ ul {
+ padding-left: 20px;
+ margin-bottom: 0;
+ }
+}
+
.calendar-hint {
margin-top: -12px;
float: right;
@@ -51,9 +74,17 @@
.profile-link-holder {
display: inline;
+ a {
+ color: $blue-dark;
+ text-decoration: none;
+ }
+}
+
+// Middle dot divider between each element in a list of items.
+.middle-dot-divider {
&:after {
- content: "\00B7";
- padding: 0px 6px;
+ content: "\00B7"; // Middle Dot
+ padding: 0 6px;
font-weight: bold;
}
@@ -63,9 +94,106 @@
padding: 0;
}
}
+}
+
+.profile-user-bio {
+ // Limits the width of the user bio for readability.
+ max-width: 750px;
+ margin: auto;
+}
+
+.user-avatar-button {
+ .file-name {
+ display: inline-block;
+ padding-left: 10px;
+ }
+}
+
+.key-list-item {
+ .key-list-item-info {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ }
+ }
+
+ .description {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+}
+
+.key-icon {
+ color: $ssh-key-icon-color;
+ font-size: $ssh-key-icon-size;
+ line-height: 42px;
+}
+
+.key-created-at {
+ line-height: 42px;
+}
+.profile-settings-content {
a {
- color: $blue-dark;
- text-decoration: none;
+ color: $md-link-color;
+ }
+}
+
+.change-username-title {
+ color: $gl-warning;
+}
+
+.remove-account-title {
+ color: $gl-danger;
+}
+
+.provider-btn-group {
+ display: inline-block;
+ margin-right: 10px;
+ border: 1px solid $provider-btn-group-border;
+ border-radius: 3px;
+
+ &:last-child {
+ margin-right: 0;
+ }
+}
+
+.provider-btn-image {
+ display: inline-block;
+ padding: 5px 10px;
+ border-right: 1px solid $provider-btn-group-border;
+
+ > img {
+ width: 20px;
+ }
+}
+
+.provider-btn {
+ display: inline-block;
+ padding: 5px 10px;
+ margin-left: -3px;
+ line-height: 22px;
+ background-color: $gray-light;
+
+ &.not-active {
+ color: $provider-btn-not-active-color;
+ }
+}
+
+.profile-settings-message {
+ line-height: 32px;
+ color: $warning-message-color;
+ background-color: $warning-message-bg;
+ border: 1px solid $warning-message-border;
+ border-radius: $border-radius-base;
+}
+
+.oauth-applications {
+ form {
+ display: inline-block;
+ }
+
+ .last-heading {
+ width: 105px;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index cff3edb7ed2..82c5069638d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -26,6 +26,23 @@
}
.project-home-panel {
+ padding-bottom: 40px;
+ border-bottom: 1px solid $border-color;
+
+ .cover-controls {
+ .project-settings-dropdown {
+ margin-left: 10px;
+ display: inline-block;
+
+ .dropdown-menu {
+ left: auto;
+ width: auto;
+ right: 0px;
+ max-width: 240px;
+ }
+ }
+ }
+
.project-identicon-holder {
margin-bottom: 16px;
@@ -39,11 +56,9 @@
}
}
- .project-home-dropdown {
- margin: 13px 0px 0;
- }
-
.notifications-btn {
+ margin-top: -28px;
+
.fa-bell {
margin-right: 6px;
}
@@ -62,53 +77,44 @@
font-weight: normal;
}
+ .visibility-icon {
+ display: inline-block;
+ margin-left: 5px;
+ font-size: 18px;
+ color: $gray;
+ }
+
p {
padding: 0 $gl-padding;
color: #5c5d5e;
}
}
- .git-clone-holder {
- max-width: 498px;
-
- .form-control {
- background: #FFF;
- font-size: 14px;
- height: 42px;
- margin-left: -1px;
- }
- }
-
- .visibility-level-label {
- @extend .btn;
- @extend .btn-gray;
-
- color: $gray;
- cursor: default;
-
- i {
- color: inherit;
- }
- }
-
- .git-clone-holder {
- display: inline-table;
- position: relative;
- }
-
.project-repo-buttons {
- margin-top: 12px;
- margin-bottom: 0px;
+ margin-top: 20px;
+ margin-bottom: 0;
.count-buttons {
display: block;
- margin-bottom: 12px;
+ margin-bottom: 20px;
+ }
+
+ .clone-row {
+ .split-repo-buttons,
+ .project-clone-holder {
+ display: inline-block;
+ }
+
+ .split-repo-buttons {
+ margin: 0 12px;
+ }
}
.btn {
@include btn-gray;
text-transform: none;
}
+
.count-with-arrow {
display: inline-block;
position: relative;
@@ -141,7 +147,7 @@
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
- border-right-color: #FFF;
+ border-right-color: #fff;
}
}
.count {
@@ -153,20 +159,20 @@
border-style: solid;
font-size: 13px;
font-weight: 600;
- line-height: 20px;
- padding: 11px 16px;
+ line-height: 13px;
+ padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
- padding: 10px;
+ padding: 10px 14px;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
white-space: nowrap;
- margin: 0 11px 0px 4px;
+ margin: 0 11px 0 4px;
&:hover {
- background: #FFF;
+ background: #fff;
}
}
}
@@ -182,141 +188,6 @@
}
}
-.git-clone-holder {
- .project-home-dropdown + & {
- margin-right: 45px;
- }
-
- .clone-options {
- display: table-cell;
- a.btn {
- width: 100%;
- }
- }
-
- .form-control {
- cursor: auto;
- @extend .monospace;
- background: #FAFAFA;
- width: 101%;
- }
-
- .input-group-addon {
- background: #f7f8fa;
-
- &.git-protocols {
- padding: 0;
- border: none;
-
- .input-group-btn:last-child > .btn {
- @include border-radius-right(0);
-
- border-left: 1px solid #c6cacf;
- margin-left: -2px !important;
- }
- }
- }
-}
-
-.projects-search-form {
-
- .input-group .form-control {
- height: 42px;
- }
-}
-
-.input-group-btn {
- .btn {
- @include btn-gray;
- @include btn-middle;
-
- &:hover {
- outline: none;
- }
-
- &:focus {
- outline: none;
- }
-
- &:active {
- outline: none;
- }
-
- &.btn-clipboard {
- padding-left: 15px;
- padding-right: 15px;
- }
- }
-
- .active {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
-
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
- }
-
- .btn-green {
- @include btn-green
- }
-
-}
-
-.split-repo-buttons {
- display: inline-table;
- margin: 0 12px 0 12px;
-
- .btn{
- @include btn-gray;
- @include btn-default;
- }
-
- .dropdown-toggle {
- margin: -5px;
- }
-}
-
-#notification-form {
- margin-left: 5px;
-}
-
-.dropdown-new {
- margin-left: -5px;
-}
-
-.open > .dropdown-new.btn {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
-
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
- text-transform: uppercase;
- color: #313236 !important;
- font-size: 13px;
- font-weight: 600;
-}
-
-.dropdown-menu {
- @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
- @include border-radius (0px);
-
- border: none;
- padding: 16px 0;
- font-size: 14px;
- font-weight: 100;
-
- li a {
- color: #5f697a;
- line-height: 30px;
-
- &:hover {
- background-color: #3084bb !important;
- }
- }
-
- i {
- margin-right: 8px;
- }
-}
-
.project-visibility-level-holder {
.radio {
margin-bottom: 10px;
@@ -345,30 +216,8 @@
color: #555;
}
-ul.nav.nav-projects-tabs {
- @extend .nav-tabs;
-
- padding-left: 8px;
-
- li {
- a {
- padding: 6px 25px;
- margin-top: 2px;
- border-color: #DDD;
- background-color: #EEE;
- text-shadow: 0 1px 1px white;
- color: #555;
- }
- &.active {
- a {
- font-weight: bold;
- }
- }
- }
-}
-
.project_member_row form {
- margin: 0px;
+ margin: 0;
}
.transfer-project .select2-container {
@@ -393,9 +242,9 @@ ul.nav.nav-projects-tabs {
.breadcrumb.repo-breadcrumb {
padding: 0;
- line-height: 42px;
background: transparent;
border: none;
+ line-height: 42px;
margin: 0;
> li + li:before {
@@ -404,36 +253,8 @@ ul.nav.nav-projects-tabs {
}
}
-.top-area {
- border-bottom: 1px solid #EEE;
- margin: 0 -16px;
- padding: 0 $gl-padding;
-
- ul.left-top-menu {
- display: inline-block;
- width: 50%;
- margin-bottom: 0px;
- border-bottom: none;
- }
-
- .projects-search-form {
- width: 50%;
- display: inline-block;
- float: right;
- padding-top: 7px;
- text-align: right;
-
- .btn-green {
- margin-top: -2px;
- margin-left: 10px;
- }
- }
-
- @media (max-width: $screen-xs-max) {
- .projects-search-form {
- padding-top: 15px;
- }
- }
+.last-push-widget {
+ margin-top: -1px;
}
.fork-namespaces {
@@ -471,12 +292,12 @@ table.table.protected-branches-list tr.no-border {
padding-top: 10px;
padding-bottom: 4px;
- ul.nav-pills {
- display:inline-block;
+ ul.nav {
+ display: inline-block;
}
- .nav-pills li {
- display:inline;
+ .nav li {
+ display: inline;
}
.nav > li > a {
@@ -489,11 +310,11 @@ table.table.protected-branches-list tr.no-border {
}
li {
- display:inline;
+ display: inline;
}
a {
- float:left;
+ float: left;
font-size: 17px;
}
@@ -511,22 +332,6 @@ pre.light-well {
border-color: #f1f1f1;
}
-.projects-search-form {
- margin: -$gl-padding;
- padding: $gl-padding;
- margin-bottom: 0px;
-
- input {
- display: inline-block;
- width: calc(100% - 151px);
- }
-
- .btn {
- display: inline-block;
- width: 135px;
- }
-}
-
.git-empty {
margin: 0 7px 0 7px;
@@ -562,41 +367,27 @@ pre.light-well {
@include basic-list;
.project-row {
- padding: $gl-padding;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
&.no-description {
.project {
- line-height: 44px;
+ line-height: 40px;
}
}
.project-full-name {
@include str-truncated;
- font-weight: 600;
- color: #4c4e54;
}
- .project-controls {
- float: right;
- color: $gl-gray;
- line-height: 45px;
- color: #7f8fa4;
+ .controls {
+ line-height: 40px;
a:hover {
text-decoration: none;
}
- }
-
- .project-description {
- color: #7f8fa4;
- p {
- @include str-truncated;
- margin-bottom: 0;
- color: #7f8fa4;
+ > span {
+ margin-left: 10px;
}
}
}
@@ -619,8 +410,6 @@ pre.light-well {
}
.project-last-commit {
- margin: 0 7px;
-
.ci-status {
margin-right: 16px;
}
@@ -650,9 +439,7 @@ pre.light-well {
}
.project-show-readme .readme-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- padding: ($gl-padding + 7px);
+ padding: $gl-padding 0;
border-top: 0;
.edit-project-readme {
@@ -660,3 +447,48 @@ pre.light-well {
position: relative;
}
}
+
+.git-clone-holder {
+ width: 498px;
+
+ .btn-clipboard {
+ border: 1px solid $border-color;
+ padding: 6px $gl-padding;
+ }
+
+ .project-home-dropdown + & {
+ margin-right: 45px;
+ }
+
+ .clone-options {
+ display: table-cell;
+ a.btn {
+ width: 100%;
+ }
+ }
+
+ .form-control {
+ @extend .monospace;
+ background: #fff;
+ font-size: 14px;
+ margin-left: -1px;
+ cursor: auto;
+ width: 101%;
+ }
+}
+
+.cannot-be-merged,
+.cannot-be-merged:hover {
+ color: #e62958;
+ margin-top: 2px;
+}
+
+.private-forks-notice .private-fork-icon {
+ i:nth-child(1) {
+ color: #2aa056;
+ }
+
+ i:nth-child(2) {
+ color: #fff;
+ }
+}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index a9111a7388f..eec22c5dc96 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -1,7 +1,7 @@
.runner-state {
padding: 6px 12px;
margin-right: 10px;
- color: #FFF;
+ color: #fff;
&.runner-state-shared {
background: #32b186;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 3aaa96da609..b6e45024644 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -1,8 +1,12 @@
.search-results {
.search-result-row {
- border-bottom: 1px solid #DDD;
- padding-bottom: 15px;
- margin-bottom: 15px;
+ border-bottom: 1px solid $border-color;
+ padding-bottom: $gl-padding;
+ margin-bottom: $gl-padding;
+
+ &:last-child {
+ border-bottom: none;
+ }
}
}
@@ -12,7 +16,7 @@
margin-bottom: 20px;
input {
- border-color: #BBB;
+ border-color: #bbb;
font-weight: bold;
}
}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index 92d84d9640f..bed6470dbd3 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -13,13 +13,13 @@ table .sherlock-code {
}
.sherlock-line-samples-table {
- margin-bottom: 0px !important;
+ margin-bottom: 0 !important;
thead tr th,
tbody tr td {
font-size: 13px !important;
text-align: right;
- padding: 0px 10px !important;
+ padding: 0 10px !important;
}
}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 1430d01859d..639d639d5b0 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -2,30 +2,6 @@
padding: 2px;
}
-
-.snippet-row {
- .snippet-title {
- font-size: 15px;
- font-weight: bold;
- line-height: 20px;
- margin-bottom: 2px;
-
- .monospace {
- font-weight: normal;
- }
- }
-
- .snippet-info {
- color: #888;
- font-size: 13px;
- line-height: 24px;
-
- a {
- color: #888;
- }
- }
-}
-
.snippet-holder {
margin-bottom: -$gl-padding;
@@ -50,5 +26,13 @@
margin-right: 10px;
font-size: $gl-font-size;
border: 1px solid;
- line-height: 40px;
+ line-height: 32px;
+}
+
+.markdown-snippet-copy {
+ position: fixed;
+ top: -10px;
+ left: -10px;
+ max-height: 0;
+ max-width: 0;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 4b6ef035673..6f777d11641 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,7 +1,7 @@
.ci-status {
padding: 2px 7px;
margin-right: 5px;
- border: 1px solid #EEE;
+ border: 1px solid #eee;
white-space: nowrap;
@include border-radius(4px);
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
new file mode 100644
index 00000000000..27970eba159
--- /dev/null
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -0,0 +1,96 @@
+/**
+ * Dashboard Todos
+ *
+ */
+
+.navbar-nav {
+ li {
+ .badge.todos-pending-count {
+ background-color: #7f8fa4;
+ 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-title {
+ @include str-truncated(calc(100% - 174px));
+ font-weight: 600;
+
+ .author-name {
+ color: #333;
+ }
+ }
+
+ .todo-body {
+ margin-right: 174px;
+
+ .todo-note {
+ word-wrap: break-word;
+
+ .md {
+ color: #7f8fa4;
+ font-size: $gl-font-size;
+
+ p {
+ color: #5c5d5e;
+ }
+ }
+
+ pre {
+ border: none;
+ background: #f9f9f9;
+ border-radius: 0;
+ color: #777;
+ margin: 0 20px;
+ overflow: hidden;
+ }
+
+ .note-image-attach {
+ margin-top: 4px;
+ margin-left: 0;
+ max-width: 200px;
+ float: none;
+ }
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .todo-item {
+ padding-left: $gl-padding;
+
+ .todo-title {
+ white-space: normal;
+ overflow: visible;
+ max-width: 100%;
+ }
+
+ .avatar {
+ display: none;
+ }
+
+ .todo-body {
+ margin: 0;
+ border-left: 2px solid #ddd;
+ padding-left: 10px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index d4ab6967ccd..73c7c9f687c 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -1,18 +1,27 @@
.tree-holder {
+ > .nav-block {
+ margin: 11px 0;
+ }
+
+ .file-finder {
+ width: 50%;
+ .file-finder-input {
+ width: 95%;
+ display: inline-block;
+ }
+ }
.tree-table {
margin-bottom: 0;
tr {
> td, > th {
- line-height: 28px;
+ line-height: 26px;
}
&:hover {
td {
- background: $hover;
- border-top: 1px solid #ADF;
- border-bottom: 1px solid #ADF;
+ background: $row-hover;
}
cursor: pointer;
}
@@ -37,7 +46,7 @@
img {
position: relative;
- top:-1px;
+ top: -1px;
}
}
@@ -78,12 +87,14 @@
.blob-commit-info {
list-style: none;
+ padding: $gl-padding;
+ background: $background-color;
+ border: 1px solid $border-color;
+ border-bottom: none;
margin: 0;
- padding: 0;
- margin-bottom: 5px;
.commit {
- padding: $gl-padding 0;
+ padding: 0;
.commit-row-title {
.commit-row-message {
@@ -107,3 +118,8 @@
font-weight: normal;
color: $md-link-color;
}
+
+.tree-controls {
+ float: right;
+ margin-top: 11px;
+}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 185f3622e64..587bd6a1e8a 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -3,4 +3,15 @@
margin: 35px 0 20px;
font-weight: bold;
}
+
+ .example {
+ &:before {
+ content: "Example";
+ color: #bbb;
+ }
+
+ padding: 15px;
+ border: 1px dashed #ddd;
+ margin-bottom: 15px;
+ }
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index cdf514197cb..dfaeba41cf6 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,8 +4,3 @@
margin-right: auto;
padding-right: 7px;
}
-
-.wiki-last-edit-by {
- font-size: 80%;
- font-weight: normal;
-}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 9a50096c0d0..8886c1dff56 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -2,23 +2,23 @@
// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
// see also: https://gist.github.com/jasonm23/2868981
- $black: #000000;
+ $black: #000;
$red: #cd0000;
$green: #00cd00;
$yellow: #cdcd00;
- $blue: #0000ee; // according to wikipedia, this is the xterm standard
+ $blue: #00e; // according to wikipedia, this is the xterm standard
//$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile)
$magenta: #cd00cd;
$cyan: #00cdcd;
$white: #e5e5e5;
$l-black: #7f7f7f;
- $l-red: #ff0000;
- $l-green: #00ff00;
- $l-yellow: #ffff00;
+ $l-red: #f00;
+ $l-green: #0f0;
+ $l-yellow: #ff0;
$l-blue: #5c5cff;
- $l-magenta: #ff00ff;
- $l-cyan: #00ffff;
- $l-white: #ffffff;
+ $l-magenta: #f0f;
+ $l-cyan: #0ff;
+ $l-white: #fff;
.term-bold {
font-weight: bold;
@@ -136,7 +136,7 @@
.xterm-fg-0 {
- color: #000000;
+ color: #000;
}
.xterm-fg-1 {
color: #800000;
@@ -163,28 +163,28 @@
color: #808080;
}
.xterm-fg-9 {
- color: #ff0000;
+ color: #f00;
}
.xterm-fg-10 {
- color: #00ff00;
+ color: #0f0;
}
.xterm-fg-11 {
- color: #ffff00;
+ color: #ff0;
}
.xterm-fg-12 {
- color: #0000ff;
+ color: #00f;
}
.xterm-fg-13 {
- color: #ff00ff;
+ color: #f0f;
}
.xterm-fg-14 {
- color: #00ffff;
+ color: #0ff;
}
.xterm-fg-15 {
- color: #ffffff;
+ color: #fff;
}
.xterm-fg-16 {
- color: #000000;
+ color: #000;
}
.xterm-fg-17 {
color: #00005f;
@@ -199,7 +199,7 @@
color: #0000d7;
}
.xterm-fg-21 {
- color: #0000ff;
+ color: #00f;
}
.xterm-fg-22 {
color: #005f00;
@@ -274,7 +274,7 @@
color: #00d7ff;
}
.xterm-fg-46 {
- color: #00ff00;
+ color: #0f0;
}
.xterm-fg-47 {
color: #00ff5f;
@@ -289,7 +289,7 @@
color: #00ffd7;
}
.xterm-fg-51 {
- color: #00ffff;
+ color: #0ff;
}
.xterm-fg-52 {
color: #5f0000;
@@ -724,7 +724,7 @@
color: #d7ffff;
}
.xterm-fg-196 {
- color: #ff0000;
+ color: #f00;
}
.xterm-fg-197 {
color: #ff005f;
@@ -739,7 +739,7 @@
color: #ff00d7;
}
.xterm-fg-201 {
- color: #ff00ff;
+ color: #f0f;
}
.xterm-fg-202 {
color: #ff5f00;
@@ -814,7 +814,7 @@
color: #ffd7ff;
}
.xterm-fg-226 {
- color: #ffff00;
+ color: #ff0;
}
.xterm-fg-227 {
color: #ffff5f;
@@ -829,7 +829,7 @@
color: #ffffd7;
}
.xterm-fg-231 {
- color: #ffffff;
+ color: #fff;
}
.xterm-fg-232 {
color: #080808;
@@ -850,7 +850,7 @@
color: #3a3a3a;
}
.xterm-fg-238 {
- color: #444444;
+ color: #444;
}
.xterm-fg-239 {
color: #4e4e4e;
@@ -901,6 +901,6 @@
color: #e4e4e4;
}
.xterm-fg-255 {
- color: #eeeeee;
+ color: #eee;
}
}
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 20bc5173f1d..2eac0cabf7a 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -2,6 +2,7 @@ class AbuseReportsController < ApplicationController
def new
@abuse_report = AbuseReport.new
@abuse_report.user_id = params[:user_id]
+ @ref_url = params.fetch(:ref_url, '')
end
def create
@@ -9,12 +10,10 @@ class AbuseReportsController < ApplicationController
@abuse_report.reporter = current_user
if @abuse_report.save
- if current_application_settings.admin_notification_email.present?
- AbuseReportMailer.notify(@abuse_report.id).deliver_later
- end
+ @abuse_report.notify
message = "Thank you for your report. A GitLab administrator will look into it shortly."
- redirect_to root_path, notice: message
+ redirect_to @abuse_report.user, notice: message
else
render :new
end
@@ -23,6 +22,9 @@ class AbuseReportsController < ApplicationController
private
def report_params
- params.require(:abuse_report).permit(:user_id, :message)
+ params.require(:abuse_report).permit(%i(
+ message
+ user_id
+ ))
end
end
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 38a5a9fca08..e9b0972bdd8 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -6,11 +6,9 @@ class Admin::AbuseReportsController < Admin::ApplicationController
def destroy
abuse_report = AbuseReport.find(params[:id])
- if params[:remove_user]
- abuse_report.user.destroy
- end
-
+ abuse_report.remove_user(deleted_by: current_user) if params[:remove_user]
abuse_report.destroy
+
render nothing: true
end
end
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
new file mode 100644
index 00000000000..26cf74e4849
--- /dev/null
+++ b/app/controllers/admin/appearances_controller.rb
@@ -0,0 +1,57 @@
+class Admin::AppearancesController < Admin::ApplicationController
+ before_action :set_appearance, except: :create
+
+ def show
+ end
+
+ def preview
+ end
+
+ def create
+ @appearance = Appearance.new(appearance_params)
+
+ if @appearance.save
+ redirect_to admin_appearances_path, notice: 'Appearance was successfully created.'
+ else
+ render action: 'show'
+ end
+ end
+
+ def update
+ if @appearance.update(appearance_params)
+ redirect_to admin_appearances_path, notice: 'Appearance was successfully updated.'
+ else
+ render action: 'show'
+ end
+ end
+
+ def logo
+ @appearance.remove_logo!
+
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.'
+ end
+
+ def header_logos
+ @appearance.remove_header_logo!
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
+ end
+
+ private
+
+ # Use callbacks to share common setup or constraints between actions.
+ def set_appearance
+ @appearance = Appearance.last || Appearance.new
+ end
+
+ # Only allow a trusted parameter "white list" through.
+ def appearance_params
+ params.require(:appearance).permit(
+ :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
+ :updated_by
+ )
+ end
+end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 10e736fd362..04a99d8c84a 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -70,14 +70,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:metrics_enabled,
:metrics_host,
:metrics_port,
- :metrics_username,
- :metrics_password,
:metrics_pool_size,
:metrics_timeout,
:metrics_method_call_threshold,
+ :metrics_sample_interval,
:recaptcha_enabled,
:recaptcha_site_key,
:recaptcha_private_key,
+ :sentry_enabled,
+ :sentry_dsn,
+ :akismet_enabled,
+ :akismet_api_key,
+ :email_author_in_body,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 497c34f8f49..fc342924987 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -1,8 +1,12 @@
class Admin::BroadcastMessagesController < Admin::ApplicationController
- before_action :broadcast_messages
+ before_action :finder, only: [:edit, :update, :destroy]
def index
- @broadcast_message = BroadcastMessage.new
+ @broadcast_messages = BroadcastMessage.reorder("ends_at DESC").page(params[:page])
+ @broadcast_message = BroadcastMessage.new
+ end
+
+ def edit
end
def create
@@ -15,8 +19,16 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
end
+ def update
+ if @broadcast_message.update(broadcast_message_params)
+ redirect_to admin_broadcast_messages_path, notice: 'Broadcast Message was successfully updated.'
+ else
+ render :edit
+ end
+ end
+
def destroy
- BroadcastMessage.find(params[:id]).destroy
+ @broadcast_message.destroy
respond_to do |format|
format.html { redirect_back_or_default(default: { action: 'index' }) }
@@ -24,16 +36,23 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
end
+ def preview
+ @message = broadcast_message_params[:message]
+ end
+
protected
- def broadcast_messages
- @broadcast_messages ||= BroadcastMessage.order("starts_at DESC").page(params[:page])
+ def finder
+ @broadcast_message = BroadcastMessage.find(params[:id])
end
def broadcast_message_params
- params.require(:broadcast_message).permit(
- :alert_type, :color, :ends_at, :font,
- :message, :starts_at
- )
+ params.require(:broadcast_message).permit(%i(
+ color
+ ends_at
+ font
+ message
+ starts_at
+ ))
end
end
diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb
index 83d9684c706..0db91eaaf2e 100644
--- a/app/controllers/admin/builds_controller.rb
+++ b/app/controllers/admin/builds_controller.rb
@@ -5,12 +5,12 @@ class Admin::BuildsController < Admin::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
@builds.finished
else
- @builds.running_or_pending.reverse_order
+ @builds
end
@builds = @builds.page(params[:page]).per(30)
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4d3e48f7f81..668396a0f20 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController
private
def group
- @group = Group.find_by(path: params[:id])
+ @group ||= Group.find_by(path: params[:id])
end
def group_params
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index e383fe38ea6..79a53556f0a 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -26,6 +26,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def update
if @identity.update_attributes(identity_params)
+ RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else
render :edit
@@ -34,6 +35,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def destroy
if @identity.destroy
+ RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.'
else
redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.'
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index 3b070e65d0d..d79ce2b10fe 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -53,6 +53,6 @@ class Admin::LabelsController < Admin::ApplicationController
end
def label_params
- params[:label].permit(:title, :color)
+ params[:label].permit(:title, :description, :color)
end
end
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
new file mode 100644
index 00000000000..377e9741e5f
--- /dev/null
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -0,0 +1,17 @@
+class Admin::SpamLogsController < Admin::ApplicationController
+ def index
+ @spam_logs = SpamLog.order(id: :desc).page(params[:page])
+ end
+
+ def destroy
+ spam_log = SpamLog.find(params[:id])
+
+ if params[:remove_user]
+ spam_log.remove_user
+ redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
+ else
+ spam_log.destroy
+ render nothing: true
+ end
+ end
+end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index d7c927d444c..9abf08d0e19 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -40,7 +40,9 @@ class Admin::UsersController < Admin::ApplicationController
end
def unblock
- if user.activate
+ if user.ldap_blocked?
+ redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab")
+ elsif user.activate
redirect_back_or_admin_user(notice: "Successfully unblocked")
else
redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked")
@@ -117,10 +119,10 @@ class Admin::UsersController < Admin::ApplicationController
end
def destroy
- DeleteUserService.new(current_user).execute(user)
+ DeleteUserWorker.perform_async(current_user.id, user.id)
respond_to do |format|
- format.html { redirect_to admin_users_path }
+ format.html { redirect_to admin_users_path, notice: "The user is being deleted." }
format.json { head :ok }
end
end
@@ -148,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController
:email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
- :projects_limit, :can_create_group, :admin, :key_id
+ :projects_limit, :can_create_group, :admin, :key_id, :external
)
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d9a37a4d45f..1f55b18e0b1 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
+ before_action :sentry_user_context
before_action :default_headers
before_action :add_gon_variables
before_action :configure_permitted_parameters, if: :devise_controller?
@@ -24,6 +25,7 @@ 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)
@@ -41,6 +43,16 @@ class ApplicationController < ActionController::Base
protected
+ def sentry_user_context
+ if Rails.env.production? && current_application_settings.sentry_enabled && current_user
+ Raven.user_context(
+ id: current_user.id,
+ email: current_user.email,
+ username: current_user.username,
+ )
+ end
+ end
+
# From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
# https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
def authenticate_user_from_token!
@@ -48,6 +60,8 @@ class ApplicationController < ActionController::Base
params[:authenticity_token].presence
elsif params[:private_token].presence
params[:private_token].presence
+ elsif request.headers['PRIVATE-TOKEN'].present?
+ request.headers['PRIVATE-TOKEN']
end
user = user_token && User.find_by_authentication_token(user_token.to_s)
@@ -115,7 +129,7 @@ class ApplicationController < ActionController::Base
# localhost/group/project
#
if id =~ /\.git\Z/
- redirect_to request.original_url.gsub(/\.git\Z/, '') and return
+ redirect_to request.original_url.gsub(/\.git\/?\Z/, '') and return
end
project_path = "#{namespace}/#{id}"
@@ -150,7 +164,7 @@ class ApplicationController < ActionController::Base
end
def git_not_found!
- render html: "errors/git_not_found", layout: "errors", status: 404
+ render "errors/git_not_found.html", layout: "errors", status: 404
end
def method_missing(method_sym, *arguments, &block)
@@ -232,6 +246,8 @@ class ApplicationController < ActionController::Base
def ldap_security_check
if current_user && current_user.requires_ldap_check?
+ return unless current_user.try_obtain_ldap_lease
+
unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
@@ -263,9 +279,10 @@ class ApplicationController < ActionController::Base
}
end
- def view_to_html_string(partial)
+ def view_to_html_string(partial, locals = {})
render_to_string(
partial,
+ locals: locals,
layout: false,
formats: [:html]
)
@@ -286,7 +303,8 @@ class ApplicationController < ActionController::Base
end
def set_filters_params
- params[:sort] ||= 'created_desc'
+ set_default_sort
+
params[:scope] = 'all' if params[:scope].blank?
params[:state] = 'opened' if params[:state].blank?
@@ -393,4 +411,31 @@ 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
+ key = if is_a_listing_page_for?('issues') || is_a_listing_page_for?('merge_requests')
+ 'issuable_sort'
+ end
+
+ cookies[key] = params[:sort] if key && params[:sort].present?
+ params[:sort] = cookies[key] if key
+ params[:sort] ||= 'id_desc'
+ end
+
+ def is_a_listing_page_for?(page_type)
+ controller_name, action_name = params.values_at(:controller, :action)
+
+ (controller_name == "projects/#{page_type}" && action_name == 'index') ||
+ (controller_name == 'groups' && action_name == page_type) ||
+ (controller_name == 'dashboard' && action_name == page_type)
+ end
end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
index c420b59c3a2..5bb7d499cdc 100644
--- a/app/controllers/ci/application_controller.rb
+++ b/app/controllers/ci/application_controller.rb
@@ -3,52 +3,5 @@ module Ci
def self.railtie_helpers_paths
"app/helpers/ci"
end
-
- private
-
- def authorize_access_project!
- unless can?(current_user, :read_project, project)
- return page_404
- end
- end
-
- def authorize_manage_builds!
- unless can?(current_user, :manage_builds, project)
- return page_404
- end
- end
-
- def authenticate_admin!
- return render_404 unless current_user.is_admin?
- end
-
- def authorize_manage_project!
- unless can?(current_user, :admin_project, project)
- return page_404
- end
- end
-
- def page_404
- render file: "#{Rails.root}/public/404.html", status: 404, layout: false
- end
-
- def default_headers
- headers['X-Frame-Options'] = 'DENY'
- headers['X-XSS-Protection'] = '1; mode=block'
- end
-
- # JSON for infinite scroll via Pager object
- def pager_json(partial, count)
- html = render_to_string(
- partial,
- layout: false,
- formats: [:html]
- )
-
- render json: {
- html: html,
- count: count
- }
- end
end
end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index e782a51e7eb..a7af3cb8345 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -6,11 +6,13 @@ module Ci
end
def create
- if params[:content].blank?
+ @content = params[:content]
+
+ if @content.blank?
@status = false
@error = "Please provide content of .gitlab-ci.yml"
else
- @config_processor = Ci::GitlabCiYamlProcessor.new params[:content]
+ @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@status = true
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index 3004c2d27f0..081e01a75e0 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,9 +1,9 @@
module Ci
class ProjectsController < Ci::ApplicationController
- before_action :project, except: [:index]
- before_action :authenticate_user!, except: [:index, :build, :badge]
- before_action :authorize_access_project!, except: [:index, :badge]
+ before_action :project
+ before_action :authorize_read_project!, except: [:badge]
before_action :no_cache, only: [:badge]
+ skip_before_action :authenticate_user!, only: [:badge]
protect_from_forgery
def show
@@ -13,9 +13,14 @@ module Ci
# Project status badge
# Image with build status for sha or ref
+ #
+ # This action in DEPRECATED, this is here only for backwards compatibility
+ # with projects migrated from GitLab CI.
+ #
def badge
- image = Ci::ImageForBuildService.new.execute(@project, params)
+ return render_404 unless @project
+ image = Ci::ImageForBuildService.new.execute(@project, params)
send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml"
end
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
new file mode 100644
index 00000000000..0a995c45bdf
--- /dev/null
+++ b/app/controllers/concerns/continue_params.rb
@@ -0,0 +1,13 @@
+module ContinueParams
+ extend ActiveSupport::Concern
+
+ def continue_params
+ continue_params = params[:continue]
+ return nil unless continue_params
+
+ continue_params = continue_params.permit(:to, :notice, :notice_now)
+ return unless continue_params[:to] && continue_params[:to].start_with?('/')
+
+ continue_params
+ end
+end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 62127a09081..787416c17ab 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -13,17 +13,11 @@ module CreatesCommit
result = service.new(@tree_edit_project, current_user, commit_params).execute
if result[:status] == :success
- flash[:notice] = success_notice || "Your changes have been successfully committed."
-
- if create_merge_request?
- success_path = new_merge_request_path
- target = different_project? ? "project" : "branch"
- flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
- end
+ update_flash_notice(success_notice)
respond_to do |format|
- format.html { redirect_to success_path }
- format.json { render json: { message: "success", filePath: success_path } }
+ format.html { redirect_to final_success_path(success_path) }
+ format.json { render json: { message: "success", filePath: final_success_path(success_path) } }
end
else
flash[:alert] = result[:message]
@@ -41,14 +35,32 @@ module CreatesCommit
end
def authorize_edit_tree!
- return if can?(current_user, :push_code, project)
- return if current_user && current_user.already_forked?(project)
+ return if can_collaborate_with_project?
access_denied!
end
private
+ def update_flash_notice(success_notice)
+ flash[:notice] = success_notice || "Your changes have been successfully committed."
+
+ if create_merge_request?
+ if merge_request_exists?
+ flash[:notice] = nil
+ else
+ target = different_project? ? "project" : "branch"
+ flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
+ end
+ end
+ end
+
+ def final_success_path(success_path)
+ return success_path unless create_merge_request?
+
+ merge_request_exists? ? existing_merge_request_path : new_merge_request_path
+ end
+
def new_merge_request_path
new_namespace_project_merge_request_path(
@mr_source_project.namespace,
@@ -62,6 +74,19 @@ module CreatesCommit
)
end
+ def existing_merge_request_path
+ namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request)
+ end
+
+ def merge_request_exists?
+ return @merge_request if defined?(@merge_request)
+
+ @merge_request = @mr_target_project.merge_requests.opened.find_by(
+ source_branch: @mr_source_branch,
+ target_branch: @mr_target_branch
+ )
+ end
+
def different_project?
@mr_source_project != @mr_target_project
end
@@ -75,7 +100,7 @@ module CreatesCommit
end
def set_commit_variables
- @mr_source_branch = @target_branch
+ @mr_source_branch ||= @target_branch
if can?(current_user, :push_code, @project)
# Edit file in this project
@@ -89,7 +114,7 @@ module CreatesCommit
else
# Merge request to this project
@mr_target_project = @project
- @mr_target_branch = @ref
+ @mr_target_branch ||= @ref
end
else
# Edit file in fork
@@ -97,7 +122,7 @@ module CreatesCommit
# Merge request from fork to this project
@mr_source_project = @tree_edit_project
@mr_target_project = @project
- @mr_target_branch = @mr_target_project.repository.root_ref
+ @mr_target_branch ||= @ref
end
end
end
diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb
new file mode 100644
index 00000000000..f63b703d101
--- /dev/null
+++ b/app/controllers/concerns/filter_projects.rb
@@ -0,0 +1,15 @@
+# == FilterProjects
+#
+# Controller concern to handle projects filtering
+# * by name
+# * by archived state
+#
+module FilterProjects
+ extend ActiveSupport::Concern
+
+ def filter_projects(projects)
+ projects = projects.search(params[:filter_projects]) if params[:filter_projects].present?
+ projects = projects.non_archived if params[:archived].blank?
+ projects
+ end
+end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index effd4721949..ef8e74a4641 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -2,10 +2,12 @@ module IssuesAction
extend ActiveSupport::Concern
def issues
- @issues = get_issues_collection
+ @issues = get_issues_collection.non_archived
@issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
@issues = @issues.preload(:author, :project)
+ @label = @issuable_finder.labels.first
+
respond_to do |format|
format.html
format.atom { render layout: false }
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index f7a25111db9..9c49596bd0b 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -2,8 +2,10 @@ module MergeRequestsAction
extend ActiveSupport::Concern
def merge_requests
- @merge_requests = get_merge_requests_collection
+ @merge_requests = get_merge_requests_collection.non_archived
@merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project)
+
+ @label = @issuable_finder.labels.first
end
end
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
new file mode 100644
index 00000000000..8a43c0b93c4
--- /dev/null
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -0,0 +1,17 @@
+module ToggleSubscriptionAction
+ extend ActiveSupport::Concern
+
+ def toggle_subscription
+ return unless current_user
+
+ subscribable_resource.toggle_subscription(current_user)
+
+ render nothing: true
+ end
+
+ private
+
+ def subscribable_resource
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 58e9049f158..0e8b63872ca 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,9 +1,15 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
+ include FilterProjects
+
before_action :event_filter
def index
- @projects = current_user.authorized_projects.sorted_by_activity.non_archived
+ @projects = current_user.authorized_projects.sorted_by_activity
+ @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
+ @projects = @projects.sort(@sort = params[:sort])
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
+
@last_push = current_user.recent_push
respond_to do |format|
@@ -13,13 +19,21 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
load_events
render layout: false
end
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
+ end
end
end
def starred
- @projects = current_user.starred_projects
+ @projects = current_user.starred_projects.sorted_by_activity
+ @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)
+
@last_push = current_user.recent_push
@groups = []
@@ -27,8 +41,9 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
format.html
format.json do
- load_events
- pager_json("events/_events", @events.count)
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
end
end
end
@@ -36,7 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def load_events
- @events = Event.in_projects(@projects.pluck(:id))
+ @events = Event.in_projects(@projects)
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
new file mode 100644
index 00000000000..7857af9c5de
--- /dev/null
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -0,0 +1,44 @@
+class Dashboard::TodosController < Dashboard::ApplicationController
+ before_action :find_todos, only: [:index, :destroy, :destroy_all]
+
+ def index
+ @todos = @todos.page(params[:page]).per(PER_PAGE)
+ end
+
+ def destroy
+ todo.done!
+
+ todo_notice = 'Todo was successfully marked as done.'
+
+ respond_to do |format|
+ 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!)
+
+ 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
+
+ private
+
+ def todo
+ @todo ||= current_user.todos.find(params[:id])
+ end
+
+ def find_todos
+ @todos = TodosFinder.new(current_user, params).execute
+ end
+end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 087da935087..139e40db180 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -23,14 +23,14 @@ class DashboardController < Dashboard::ApplicationController
protected
def load_events
- project_ids =
+ projects =
if params[:filter] == "starred"
current_user.starred_projects
else
current_user.authorized_projects
- end.pluck(:id)
+ end
- @events = Event.in_projects(project_ids)
+ @events = Event.in_projects(projects)
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
new file mode 100644
index 00000000000..1bec5a7d27f
--- /dev/null
+++ b/app/controllers/emojis_controller.rb
@@ -0,0 +1,6 @@
+class EmojisController < ApplicationController
+ layout false
+
+ def index
+ end
+end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 9575a87ee41..a9bf4321f73 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,6 +1,6 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = GroupsFinder.new.execute(current_user)
+ @groups = Group.order_id_desc
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page]).per(PER_PAGE)
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index a5aeaed66c5..8271ca87436 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -1,24 +1,53 @@
class Explore::ProjectsController < Explore::ApplicationController
+ include FilterProjects
+
def index
@projects = ProjectsFinder.new.execute(current_user)
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
- @projects = @projects.non_archived
- @projects = @projects.search(params[:search]) if params[:search].present?
+ @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
+ end
+ end
end
def trending
- @trending_projects = TrendingProjectsFinder.new.execute(current_user)
- @trending_projects = @trending_projects.non_archived
- @trending_projects = @trending_projects.page(params[:page]).per(PER_PAGE)
+ @projects = TrendingProjectsFinder.new.execute(current_user)
+ @projects = filter_projects(@projects)
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
+ end
+ end
end
def starred
- @starred_projects = ProjectsFinder.new.execute(current_user)
- @starred_projects = @starred_projects.reorder('star_count DESC')
- @starred_projects = @starred_projects.page(params[:page]).per(PER_PAGE)
+ @projects = ProjectsFinder.new.execute(current_user)
+ @projects = filter_projects(@projects)
+ @projects = @projects.reorder('star_count DESC')
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
+ end
+ end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index fb26a4e6fc3..06c5c8be9a5 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,19 +1,21 @@
class GroupsController < Groups::ApplicationController
+ include FilterProjects
include IssuesAction
include MergeRequestsAction
- skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests]
respond_to :html
- before_action :group, except: [:new, :create]
+
+ skip_before_action :authenticate_user!, only: [:index, :show, :issues, :merge_requests]
+ before_action :group, except: [:index, :new, :create]
# Authorize
- before_action :authorize_read_group!, except: [:show, :new, :create, :autocomplete]
+ 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: [:new, :create, :projects, :edit, :update, :autocomplete]
- before_action :event_filter, only: :show
+ before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
+ before_action :event_filter, only: [:activity]
layout :determine_layout
@@ -40,13 +42,19 @@ 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?
+
+ @shared_projects = @group.shared_projects
respond_to do |format|
format.html
format.json do
- load_events
- pager_json("events/_events", @events.count)
+ render json: {
+ html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ }
end
format.atom do
@@ -56,6 +64,17 @@ class GroupsController < Groups::ApplicationController
end
end
+ def activity
+ respond_to do |format|
+ format.html
+
+ format.json do
+ load_events
+ pager_json("events/_events", @events.count)
+ end
+ end
+ end
+
def edit
end
@@ -81,14 +100,11 @@ class GroupsController < Groups::ApplicationController
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.non_archived
- end
-
- def project_ids
- @projects.pluck(:id)
+ @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity
end
# Dont allow unauthorized access to group
@@ -119,11 +135,11 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
end
def load_events
- @events = Event.in_projects(project_ids)
+ @events = Event.in_projects(@projects)
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index dc22101cd5e..d1e4ac10f6c 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
layout 'profile'
def index
- head :forbidden and return
+ set_index_vars
end
def create
@@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application)
else
- render :new
+ set_index_vars
+ render :index
end
end
- def destroy
- if @application.destroy
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy])
- end
-
- redirect_to applications_profile_url
- end
-
private
def verify_user_oauth_applications_enabled
@@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
redirect_to applications_profile_url
end
+ def set_index_vars
+ @applications = current_user.oauth_applications
+ @authorized_tokens = current_user.oauth_authorized_tokens
+ @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
+ @authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?)
+
+ # Don't overwrite a value possibly set by `create`
+ @application ||= Doorkeeper::Application.new
+ end
+
+ # Override Doorkeeper to scope to the current user
def set_application
@application = current_user.oauth_applications.find(params[:id])
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 4cad98b8e98..21135f7d607 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -1,4 +1,5 @@
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
+ include AuthenticatesWithTwoFactor
protect_from_forgery except: [:kerberos, :saml, :cas3]
@@ -21,21 +22,46 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
- @user = Gitlab::LDAP::User.new(oauth)
- @user.save if @user.changed? # will also save new users
- gl_user = @user.gl_user
- gl_user.remember_me = params[:remember_me] if @user.persisted?
+ ldap_user = Gitlab::LDAP::User.new(oauth)
+ ldap_user.save if ldap_user.changed? # will also save new users
+
+ @user = ldap_user.gl_user
+ @user.remember_me = params[:remember_me] if ldap_user.persisted?
# Do additional LDAP checks for the user filter and EE features
- if @user.allowed?
- log_audit_event(gl_user, with: :ldap)
- sign_in_and_redirect(gl_user)
+ if ldap_user.allowed?
+ if @user.two_factor_enabled?
+ prompt_for_two_factor(@user)
+ else
+ log_audit_event(@user, with: :ldap)
+ sign_in_and_redirect(@user)
+ end
else
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
end
end
+ def saml
+ if current_user
+ log_audit_event(current_user, with: :saml)
+ # Update SAML identity if data has changed.
+ identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml)
+ if identity.nil?
+ current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
+ redirect_to profile_account_path, notice: 'Authentication method updated'
+ else
+ redirect_to after_sign_in_path_for(current_user)
+ end
+ else
+ saml_user = Gitlab::Saml::User.new(oauth)
+ saml_user.save
+ @user = saml_user.gl_user
+
+ continue_login_process
+ end
+ end
+
def omniauth_error
@provider = params[:provider]
@error = params[:error]
@@ -59,25 +85,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
- @user = Gitlab::OAuth::User.new(oauth)
- @user.save
+ oauth_user = Gitlab::OAuth::User.new(oauth)
+ oauth_user.save
+ @user = oauth_user.gl_user
- # Only allow properly saved users to login.
- if @user.persisted? && @user.valid?
- log_audit_event(@user.gl_user, with: oauth['provider'])
- sign_in_and_redirect(@user.gl_user)
- else
- error_message =
- if @user.gl_user.errors.any?
- @user.gl_user.errors.map do |attribute, message|
- "#{attribute} #{message}"
- end.join(", ")
- else
- ''
- end
-
- redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
- end
+ continue_login_process
end
rescue Gitlab::OAuth::SignupDisabledError
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
@@ -98,6 +110,18 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket
end
+ def continue_login_process
+ # Only allow properly saved users to login.
+ if @user.persisted? && @user.valid?
+ log_audit_event(@user, with: oauth['provider'])
+ sign_in_and_redirect(@user)
+ else
+ error_message = @user.errors.full_messages.to_sentence
+
+ redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
+ end
+ end
+
def oauth
@oauth ||= request.env['omniauth.auth']
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index f74daff3bd0..a8575e037e4 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -23,6 +23,14 @@ class PasswordsController < Devise::PasswordsController
end
end
+ def update
+ super do |resource|
+ if resource.valid? && resource.require_password?
+ resource.update_attribute(:password_automatically_set, false)
+ end
+ end
+ end
+
protected
def resource_from_email
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f3224148fda..b88c080352b 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -3,23 +3,21 @@ class Profiles::KeysController < Profiles::ApplicationController
def index
@keys = current_user.keys
+ @key = Key.new
end
def show
@key = current_user.keys.find(params[:id])
end
- def new
- @key = current_user.keys.new
- end
-
def create
@key = current_user.keys.new(key_params)
if @key.save
redirect_to profile_key_path(@key)
else
- render 'new'
+ @keys = current_user.keys.select(&:persisted?)
+ render :index
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 6e91d9b4ad9..8f83fdd02bc 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -12,11 +12,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed?
- if two_factor_grace_period_expired?
- flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.'
- else
- grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
- flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}."
+ if two_factor_authentication_required?
+ if two_factor_grace_period_expired?
+ flash.now[:alert] = 'You must enable Two-factor Authentication for your account.'
+ else
+ grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
+ flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}."
+ end
end
@qr_code = build_qr_code
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 28803164fcf..32fca6b838e 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -8,13 +8,6 @@ class ProfilesController < Profiles::ApplicationController
def show
end
- def applications
- @applications = current_user.oauth_applications
- @authorized_tokens = current_user.oauth_authorized_tokens
- @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
- @authorized_apps = @authorized_tokens.map(&:application).uniq - [nil]
- end
-
def update
user_params.except!(:email) if @user.ldap_user?
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index dd32d509191..a326bc58215 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -28,6 +28,11 @@ class Projects::ApplicationController < ApplicationController
private
+ def apply_diff_view_cookie!
+ view = params[:view] || cookies[:diff_view]
+ cookies.permanent[:diff_view] = params[:view] = view if view
+ end
+
def builds_enabled
return render_404 unless @project.builds_enabled?
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
new file mode 100644
index 00000000000..cfea1266516
--- /dev/null
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -0,0 +1,46 @@
+class Projects::ArtifactsController < Projects::ApplicationController
+ layout 'project'
+ before_action :authorize_read_build!
+
+ def download
+ unless artifacts_file.file_storage?
+ return redirect_to artifacts_file.url
+ end
+
+ unless artifacts_file.exists?
+ return render_404
+ end
+
+ send_file artifacts_file.path, disposition: 'attachment'
+ end
+
+ def browse
+ return render_404 unless build.artifacts?
+
+ directory = params[:path] ? "#{params[:path]}/" : ''
+ @entry = build.artifacts_metadata_entry(directory)
+
+ return render_404 unless @entry.exists?
+ end
+
+ def file
+ entry = build.artifacts_metadata_entry(params[:path])
+
+ if entry.exists?
+ render json: { archive: build.artifacts_file.path,
+ entry: Base64.encode64(entry.path) }
+ else
+ render json: {}, status: 404
+ end
+ end
+
+ private
+
+ def build
+ @build ||= project.builds.unscoped.find_by!(id: params[:build_id])
+ end
+
+ def artifacts_file
+ @artifacts_file ||= build.artifacts_file
+ end
+end
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 548f1b9ebfe..a6bebc46b06 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -1,16 +1,19 @@
class Projects::AvatarsController < Projects::ApplicationController
+ include BlobHelper
+
before_action :project
def show
- @blob = @project.repository.blob_at_branch('master', @project.avatar_in_git)
+ @blob = @repository.blob_at_branch('master', @project.avatar_in_git)
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
- send_data(
- @blob.data,
- type: @blob.mime_type,
- disposition: 'inline',
- filename: @blob.name
- )
+
+ return if cached_blob?
+
+ headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
+ headers['Content-Disposition'] = 'inline'
+ headers['Content-Type'] = safe_content_type(@blob)
+ head :ok # 'render nothing: true' messes up the Content-Type
else
render_404
end
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
new file mode 100644
index 00000000000..6ff47c4033a
--- /dev/null
+++ b/app/controllers/projects/badges_controller.rb
@@ -0,0 +1,13 @@
+class Projects::BadgesController < Projects::ApplicationController
+ before_action :no_cache_headers
+
+ def build
+ 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')
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 9ea518e6c85..f576d0be1fc 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -8,28 +8,6 @@ class Projects::BlameController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@commit.id, @path)
- @blame = group_blame_lines
- end
-
- def group_blame_lines
- blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path)
-
- prev_sha = nil
- groups = []
- current_group = nil
-
- blame.each do |commit, line|
- if prev_sha && prev_sha == commit.sha
- current_group[:lines] << line
- else
- groups << current_group if current_group.present?
- current_group = { commit: commit, lines: [line] }
- end
-
- prev_sha = commit.sha
- end
-
- groups << current_group if current_group.present?
- groups
+ @blame_groups = Gitlab::Blame.new(@blob, @commit).groups
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index c56a3497bb2..cd8b2911674 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -33,6 +33,7 @@ class Projects::BlobController < Projects::ApplicationController
def edit
@last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha
+ blob.load_all_data!(@repository)
end
def update
@@ -51,8 +52,11 @@ class Projects::BlobController < Projects::ApplicationController
def preview
@content = params[:content]
+ @blob.load_all_data!(@repository)
diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
- @diff_lines = Gitlab::Diff::Parser.new.parse(diffy.diff.scan(/.*\n/))
+ diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
+ diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
+ @diff_lines = Gitlab::Diff::Highlight.new(diff_lines).highlight
render layout: false
end
@@ -65,8 +69,9 @@ class Projects::BlobController < Projects::ApplicationController
end
def diff
- @form = UnfoldForm.new(params)
- @lines = @blob.data.lines[@form.since - 1..@form.to - 1]
+ @form = UnfoldForm.new(params)
+ @lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path)
+ @lines = @lines[@form.since - 1..@form.to - 1]
if @form.bottom?
@match_line = ''
@@ -82,7 +87,7 @@ class Projects::BlobController < Projects::ApplicationController
private
def blob
- @blob ||= @repository.blob_at(@commit.id, @path)
+ @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
if @blob
@blob
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 3c2849a7601..43ea717cbd2 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -9,6 +9,11 @@ class Projects::BranchesController < Projects::ApplicationController
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
@branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
+
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
end
def recent
@@ -18,11 +23,15 @@ class Projects::BranchesController < Projects::ApplicationController
def create
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
- ref = sanitize(strip_tags(params[:ref]))
- ref = Addressable::URI.unescape(ref)
+
result = CreateBranchService.new(project, current_user).
execute(branch_name, ref)
+ if params[:issue_iid]
+ issue = @project.issues.find_by(iid: params[:issue_iid])
+ SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
+ end
+
if result[:status] == :success
@branch = result[:branch]
redirect_to namespace_project_tree_path(@project.namespace, @project,
@@ -44,4 +53,15 @@ class Projects::BranchesController < Projects::ApplicationController
format.js { render status: status[:return_code] }
end
end
+
+ private
+
+ def ref
+ if params[:ref]
+ ref_escaped = sanitize(strip_tags(params[:ref]))
+ Addressable::URI.unescape(ref_escaped)
+ else
+ @project.default_branch
+ end
+ end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 26ba12520c7..f159e169f6d 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,10 +1,8 @@
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
-
- before_action :authorize_manage_builds!, except: [:index, :show, :status]
- before_action :authorize_download_build_artifacts!, only: [:download]
-
- layout "project"
+ before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry]
+ before_action :authorize_update_build!, except: [:index, :show, :status]
+ layout 'project'
def index
@scope = params[:scope]
@@ -12,19 +10,18 @@ class Projects::BuildsController < Projects::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
@builds.finished
else
- @builds.running_or_pending.reverse_order
+ @builds
end
@builds = @builds.page(params[:page]).per(30)
end
def cancel_all
@project.builds.running_or_pending.each(&:cancel)
-
redirect_to namespace_project_builds_path(project.namespace, project)
end
@@ -43,34 +40,26 @@ class Projects::BuildsController < Projects::ApplicationController
def retry
unless @build.retryable?
- return page_404
+ return render_404
end
build = Ci::Build.retry(@build)
-
redirect_to build_path(build)
end
- def download
- unless artifacts_file.file_storage?
- return redirect_to artifacts_file.url
- end
-
- unless artifacts_file.exists?
- return not_found!
- end
-
- send_file artifacts_file.path, disposition: 'attachment'
+ def cancel
+ @build.cancel
+ redirect_to build_path(@build)
end
def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end
- def cancel
- @build.cancel
-
- redirect_to build_path(@build)
+ def erase
+ @build.erase(erased_by: current_user)
+ redirect_to namespace_project_build_path(project.namespace, project, @build),
+ notice: "Build has been sucessfully erased!"
end
private
@@ -79,27 +68,7 @@ class Projects::BuildsController < Projects::ApplicationController
@build ||= project.builds.unscoped.find_by!(id: params[:id])
end
- def artifacts_file
- build.artifacts_file
- end
-
def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build)
end
-
- def authorize_manage_builds!
- unless can?(current_user, :manage_builds, project)
- return page_404
- end
- end
-
- def authorize_download_build_artifacts!
- unless can?(current_user, :download_build_artifacts, @project)
- if current_user.nil?
- return authenticate_user!
- else
- return render_404
- end
- end
- end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 0aaba3792bf..576fa3cedb2 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -2,16 +2,20 @@
#
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
+ include CreatesCommit
+ include DiffHelper
+
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!, except: [:cancel_builds]
- before_action :authorize_manage_builds!, only: [:cancel_builds]
+ before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
+ before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
+ before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
- before_action :authorize_manage_builds!, only: [:cancel_builds, :retry_builds]
before_action :define_show_vars, only: [:show, :builds]
+ before_action :authorize_edit_tree!, only: [:revert]
def show
- return git_not_found! unless @commit
+ apply_diff_view_cookie!
@line_notes = commit.notes.inline
@note = @project.build_commit_note(commit)
@@ -55,8 +59,37 @@ class Projects::CommitController < Projects::ApplicationController
render layout: false
end
+ def revert
+ assign_revert_commit_vars
+
+ return render_404 if @target_branch.blank?
+
+ create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.",
+ success_path: successful_revert_path, failure_path: failed_revert_path)
+ end
+
private
+ def revert_type_title
+ @commit.merged_merge_request ? 'merge request' : 'commit'
+ end
+
+ def successful_revert_path
+ return referenced_merge_request_url if @commit.merged_merge_request
+
+ namespace_project_commits_url(@project.namespace, @project, @target_branch)
+ end
+
+ def failed_revert_path
+ return referenced_merge_request_url if @commit.merged_merge_request
+
+ namespace_project_commit_url(@project.namespace, @project, params[:id])
+ end
+
+ def referenced_merge_request_url
+ namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request)
+ end
+
def commit
@commit ||= @project.commit(params[:id])
end
@@ -66,20 +99,27 @@ class Projects::CommitController < Projects::ApplicationController
end
def define_show_vars
- if params[:w].to_i == 1
- @diffs = commit.diffs({ ignore_whitespace_change: true })
- else
- @diffs = commit.diffs
- end
+ return git_not_found! unless commit
+
+ opts = diff_options
+ opts[:ignore_whitespace_change] = true if params[:format] == 'diff'
+ @diffs = commit.diffs(opts)
+ @diff_refs = [commit.parent || commit, commit]
@notes_count = commit.notes.count
@statuses = ci_commit.statuses if ci_commit
end
- def authorize_manage_builds!
- unless can?(current_user, :manage_builds, project)
- return page_404
- end
+ def assign_revert_commit_vars
+ @commit = project.commit(params[:id])
+ @target_branch = params[:target_branch]
+ @mr_source_branch = @commit.revert_branch_name
+ @mr_target_branch = @target_branch
+ @commit_params = {
+ commit: @commit,
+ revert_type_title: revert_type_title,
+ create_merge_request: params[:create_merge_request].present? || different_project?
+ }
end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 04a88990bf4..1420b96840c 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -8,13 +8,22 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :authorize_download_code!
def show
- @repo = @project.repository
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
+ search = params[:search]
+
+ @commits =
+ if search.present?
+ @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact
+ else
+ @repository.commits(@ref, @path, @limit, @offset)
+ end
- @commits = @repo.commits(@ref, @path, @limit, @offset)
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
+ @merge_request = @project.merge_requests.opened.
+ find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
+
respond_to do |format|
format.html
format.json { pager_json("projects/commits/_commits", @commits.size) }
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 5200d609cc9..671d5c23024 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -1,27 +1,27 @@
require 'addressable/uri'
class Projects::CompareController < Projects::ApplicationController
+ include DiffHelper
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
+ before_action :assign_ref_vars, only: [:index, :show]
+ before_action :merge_request, only: [:index, :show]
def index
- @ref = Addressable::URI.unescape(params[:to])
end
def show
- base_ref = Addressable::URI.unescape(params[:from])
- @ref = head_ref = Addressable::URI.unescape(params[:to])
- diff_options = { ignore_whitespace_change: true } if params[:w] == '1'
-
- compare_result = CompareService.new.
- execute(@project, head_ref, @project, base_ref, diff_options)
-
- if compare_result
- @commits = Commit.decorate(compare_result.commits, @project)
- @diffs = compare_result.diffs
- @commit = @project.commit(head_ref)
- @first_commit = @project.commit(base_ref)
+ compare = CompareService.new.
+ execute(@project, @head_ref, @project, @base_ref, diff_options)
+
+ if compare
+ @commits = Commit.decorate(compare.commits, @project)
+ @commit = @project.commit(@head_ref)
+ @base_commit = @project.merge_base_commit(@base_ref, @head_ref)
+ @diffs = compare.diffs(diff_options)
+ @diff_refs = [@base_commit, @commit]
@line_notes = []
end
end
@@ -30,4 +30,16 @@ class Projects::CompareController < Projects::ApplicationController
redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
end
+
+ private
+
+ def assign_ref_vars
+ @base_ref = Addressable::URI.unescape(params[:from])
+ @ref = @head_ref = Addressable::URI.unescape(params[:to])
+ end
+
+ def merge_request
+ @merge_request ||= @project.merge_requests.opened.
+ find_by(source_project: @project, source_branch: @head_ref, target_branch: @base_ref)
+ end
end
diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb
new file mode 100644
index 00000000000..54a0c447aee
--- /dev/null
+++ b/app/controllers/projects/find_file_controller.rb
@@ -0,0 +1,26 @@
+# Controller for viewing a repository's file structure
+class Projects::FindFileController < Projects::ApplicationController
+ include ExtractsPath
+ include ActionView::Helpers::SanitizeHelper
+ include TreeHelper
+
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
+
+ def show
+ return render_404 unless @repository.commit(@ref)
+
+ respond_to do |format|
+ format.html
+ end
+ end
+
+ def list
+ file_paths = @repo.ls_files(@ref)
+
+ respond_to do |format|
+ format.json { render json: file_paths }
+ end
+ end
+end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 750181f0c19..a1b8632df98 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -1,8 +1,33 @@
class Projects::ForksController < Projects::ApplicationController
+ include ContinueParams
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
+ def index
+ base_query = project.forks.includes(:creator)
+
+ @forks = base_query.merge(ProjectsFinder.new.execute(current_user))
+ @total_forks_count = base_query.size
+ @private_forks_count = @total_forks_count - @forks.size
+ @public_forks_count = @total_forks_count - @private_forks_count
+
+ @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)
+
+ respond_to do |format|
+ format.html
+
+ format.json do
+ render json: {
+ html: view_to_html_string("projects/forks/_projects", projects: @forks)
+ }
+ end
+ end
+ end
+
def new
@namespaces = current_user.manageable_namespaces
@namespaces.delete(@project.namespace)
@@ -10,7 +35,7 @@ class Projects::ForksController < Projects::ApplicationController
def create
namespace = Namespace.find(params[:namespace_key])
-
+
@forked_project = namespace.projects.find_by(path: project.path)
@forked_project = nil unless @forked_project && @forked_project.forked_from_project == project
@@ -23,22 +48,11 @@ class Projects::ForksController < Projects::ApplicationController
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
- redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked."
+ redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project '#{@forked_project.name}' was successfully forked."
end
end
else
render :error
end
end
-
- private
-
- def continue_params
- continue_params = params[:continue]
- if continue_params
- continue_params.permit(:to, :notice, :notice_now)
- else
- nil
- end
- end
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
new file mode 100644
index 00000000000..4159e53bfa9
--- /dev/null
+++ b/app/controllers/projects/group_links_controller.rb
@@ -0,0 +1,23 @@
+class Projects::GroupLinksController < Projects::ApplicationController
+ layout 'project_settings'
+ before_action :authorize_admin_project!
+
+ def index
+ @group_links = project.project_group_links.all
+ end
+
+ def create
+ link = project.project_group_links.new
+ link.group_id = params[:link_group_id]
+ link.group_access = params[:link_group_access]
+ link.save
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+
+ def destroy
+ project.project_group_links.find(params[:id]).destroy
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 8d8035ef5ff..7756f0f0ed3 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -1,8 +1,11 @@
class Projects::ImportsController < Projects::ApplicationController
+ include ContinueParams
+
# Authorize
before_action :authorize_admin_project!
- before_action :require_no_repo, except: :show
- before_action :redirect_if_progress, except: :show
+ before_action :require_no_repo, only: [:new, :create]
+ before_action :redirect_if_progress, only: [:new, :create]
+ before_action :redirect_if_no_import, only: :show
def new
end
@@ -24,11 +27,11 @@ class Projects::ImportsController < Projects::ApplicationController
end
def show
- if @project.repository_exists? || @project.import_finished?
+ if @project.import_finished?
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
- redirect_to project_path(@project), notice: "The project was successfully forked."
+ redirect_to namespace_project_path(@project.namespace, @project), notice: finished_notice
end
elsif @project.import_failed?
redirect_to new_namespace_project_import_path(@project.namespace, @project)
@@ -36,31 +39,36 @@ class Projects::ImportsController < Projects::ApplicationController
if continue_params && continue_params[:notice_now]
flash.now[:notice] = continue_params[:notice_now]
end
+
# Render
end
end
private
- def continue_params
- continue_params = params[:continue]
- if continue_params
- continue_params.permit(:to, :notice, :notice_now)
+ def finished_notice
+ if @project.forked?
+ 'The project was successfully forked.'
else
- nil
+ 'The project was successfully imported.'
end
end
def require_no_repo
- if @project.repository_exists? && !@project.import_in_progress?
- redirect_to(namespace_project_path(@project.namespace, @project))
+ if @project.repository_exists?
+ redirect_to namespace_project_path(@project.namespace, @project)
end
end
def redirect_if_progress
if @project.import_in_progress?
- redirect_to namespace_project_import_path(@project.namespace, @project) &&
- return
+ redirect_to namespace_project_import_path(@project.namespace, @project)
+ end
+ end
+
+ def redirect_if_no_import
+ if @project.repository_exists? && @project.no_import?
+ redirect_to namespace_project_path(@project.namespace, @project)
end
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b59b52291fb..6603f28a082 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,9 +1,11 @@
class Projects::IssuesController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :module_enabled
- before_action :issue, only: [:edit, :update, :show, :toggle_subscription]
+ 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]
@@ -32,6 +34,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
@issues = @issues.page(params[:page]).per(PER_PAGE)
+ @label = @project.labels.find_by(title: params[:label_name])
respond_to do |format|
format.html
@@ -49,7 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
assignee_id: ""
)
- @issue = @project.issues.new(issue_params)
+ @issue = @noteable = @project.issues.new(issue_params)
respond_with(@issue)
end
@@ -61,7 +64,8 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
- @merge_requests = @issue.referenced_merge_requests
+ @merge_requests = @issue.referenced_merge_requests(current_user)
+ @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
respond_with(@issue)
end
@@ -109,12 +113,6 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end
- def toggle_subscription
- @issue.toggle_subscription(current_user)
-
- render nothing: true
- end
-
def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end
@@ -128,6 +126,11 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_old
end
end
+ alias_method :subscribable_resource, :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)
@@ -159,7 +162,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 86d6e3e0f6b..40d8098690a 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -1,8 +1,12 @@
class Projects::LabelsController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_read_label!
- before_action :authorize_admin_labels!, except: [:index]
+ before_action :authorize_admin_labels!, only: [
+ :new, :create, :edit, :update, :generate, :destroy
+ ]
respond_to :js, :html
@@ -69,12 +73,13 @@ class Projects::LabelsController < Projects::ApplicationController
end
def label_params
- params.require(:label).permit(:title, :color)
+ params.require(:label).permit(:title, :description, :color)
end
def label
- @label = @project.labels.find(params[:id])
+ @label ||= @project.labels.find(params[:id])
end
+ alias_method :subscribable_resource, :label
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ab5c953189c..61b82c9db46 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,8 +1,11 @@
class Projects::MergeRequestsController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+ include DiffHelper
+
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
- :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds
+ :ci_status, :cancel_merge_when_build_succeeds
]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
@@ -34,6 +37,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
@merge_requests = @merge_requests.preload(:target_project)
+ @label = @project.labels.find_by(title: params[:label_name])
+
respond_to do |format|
format.html
format.json do
@@ -57,8 +62,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def diffs
+ apply_diff_view_cookie!
+
@commit = @merge_request.last_commit
- @first_commit = @merge_request.first_commit
+ @base_commit = @merge_request.diff_base_commit
+
+ # MRs created before 8.4 don't have a diff_base_commit,
+ # but we need it for the "View file @ ..." link by deleted files
+ @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit
@comments_allowed = @reply_allowed = true
@comments_target = {
@@ -90,6 +101,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def new
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+ @noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
@@ -101,8 +113,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.last_commit
- @first_commit = @merge_request.first_commit
- @diffs = @merge_request.compare_diffs
+ @base_commit = @merge_request.diff_base_commit
+ @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
@ci_commit = @merge_request.ci_commit
@statuses = @ci_commit.statuses if @ci_commit
@@ -153,7 +165,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_check
- @merge_request.check_if_can_be_merged if @merge_request.unchecked?
+ @merge_request.check_if_can_be_merged
render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end
@@ -172,6 +184,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
+ TodoService.new.merge_merge_request(merge_request, current_user)
+
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
@@ -220,12 +234,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
- def toggle_subscription
- @merge_request.toggle_subscription(current_user)
-
- render nothing: true
- end
-
protected
def selected_target_project
@@ -239,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
+ alias_method :subscribable_resource, :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 15506bd677a..da46731d945 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -11,11 +11,12 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to :html
def index
- @milestones = case params[:state]
- when 'all'; @project.milestones.order("state, due_date DESC")
- when 'closed'; @project.milestones.closed.order("due_date DESC")
- else @project.milestones.active.order("due_date ASC")
- end
+ @milestones =
+ case params[:state]
+ when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc)
+ when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc)
+ else @project.milestones.active.reorder(due_date: :asc, title: :asc)
+ end
@milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page]).per(PER_PAGE)
@@ -31,9 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def show
- @issues = @milestone.issues
- @users = @milestone.participants.uniq
- @merge_requests = @milestone.merge_requests
end
def create
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 6f1e186d408..1b9dd568043 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -11,11 +11,9 @@ class Projects::NotesController < Projects::ApplicationController
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes.each do |note|
- notes_json[:notes] << {
- id: note.id,
- html: note_to_html(note),
- valid: note.valid?
- }
+ next if note.cross_reference_not_visible_for?(current_user)
+
+ notes_json[:notes] << note_json(note)
end
render json: notes_json
@@ -25,7 +23,7 @@ class Projects::NotesController < Projects::ApplicationController
@note = Notes::CreateService.new(project, current_user, note_params).execute
respond_to do |format|
- format.json { render_note_json(@note) }
+ format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
@@ -34,7 +32,7 @@ class Projects::NotesController < Projects::ApplicationController
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
respond_to do |format|
- format.json { render_note_json(@note) }
+ format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
@@ -99,6 +97,8 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_to_discussion_html(note)
+ return unless note.for_diff_line?
+
if params[:view] == 'parallel'
template = "projects/notes/_diff_notes_with_reply_parallel"
locals =
@@ -106,7 +106,7 @@ class Projects::NotesController < Projects::ApplicationController
{ notes_left: [note], notes_right: [] }
else
{ notes_left: [], notes_right: [note] }
- end
+ end
else
template = "projects/notes/_diff_notes_with_reply"
locals = { notes: [note] }
@@ -131,9 +131,9 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- def render_note_json(note)
+ def note_json(note)
if note.valid?
- render json: {
+ {
valid: true,
id: note.id,
discussion_id: note.discussion_id,
@@ -144,7 +144,7 @@ class Projects::NotesController < Projects::ApplicationController
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
}
else
- render json: {
+ {
valid: false,
award: note.is_award,
errors: note.errors
@@ -163,8 +163,6 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- private
-
def find_current_user_notes
@notes = NotesFinder.new.execute(project, current_user, params)
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 8364fc293b7..e7bddc4a6f1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
@project_member = @project.project_members.new
+ @project_group_links = @project.project_group_links
end
def create
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index be7d5c187fe..10de0e60530 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -1,6 +1,7 @@
# Controller for viewing a file's raw
class Projects::RawController < Projects::ApplicationController
include ExtractsPath
+ include BlobHelper
before_action :require_non_empty_project
before_action :assign_ref_vars
@@ -12,10 +13,15 @@ class Projects::RawController < Projects::ApplicationController
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
+ return if cached_blob?
+
if @blob.lfs_pointer?
send_lfs_object
else
- stream_data
+ headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
+ headers['Content-Disposition'] = 'inline'
+ headers['Content-Type'] = safe_content_type(@blob)
+ head :ok # 'render nothing: true' messes up the Content-Type
end
else
render_404
@@ -24,26 +30,6 @@ class Projects::RawController < Projects::ApplicationController
private
- def get_blob_type
- if @blob.text?
- 'text/plain; charset=utf-8'
- elsif @blob.image?
- @blob.content_type
- else
- 'application/octet-stream'
- end
- end
-
- def stream_data
- type = get_blob_type
-
- send_data(
- @blob.data,
- type: type,
- disposition: 'inline'
- )
- end
-
def send_lfs_object
lfs_object = find_lfs_object
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index c4e18c17077..00df1c9c965 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -20,6 +20,8 @@ class Projects::RefsController < Projects::ApplicationController
namespace_project_network_path(@project.namespace, @project, @id, @options)
when "graphs"
namespace_project_graph_path(@project.namespace, @project, @id)
+ when "find_file"
+ namespace_project_find_file_path(@project.namespace, @project, @id)
when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id)
else
@@ -62,9 +64,9 @@ class Projects::RefsController < Projects::ApplicationController
}
end
- if @logs.present?
- @log_url = namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))
- @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))
+ offset = (@offset + @limit)
+ if contents.size > offset
+ @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: offset)
end
respond_to do |format|
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index ba9aea1c165..5c7614cfbaf 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -11,7 +11,9 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
- render json: ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute
+ RepositoryArchiveCacheWorker.perform_async
+ headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
+ head :ok
rescue => ex
logger.error("#{self.class.name}: #{ex}")
return git_not_found!
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index e2785caa2fb..bedeb4a295c 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -1,5 +1,5 @@
class Projects::RunnerProjectsController < Projects::ApplicationController
- before_action :authorize_admin_project!
+ before_action :authorize_admin_build!
layout 'project_settings'
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 4993b2648a5..0dd2d6a99be 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -1,6 +1,6 @@
class Projects::RunnersController < Projects::ApplicationController
+ before_action :authorize_admin_build!
before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
- before_action :authorize_admin_project!
layout 'project_settings'
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 2104c7a7a71..92b0caa2efb 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -25,7 +25,7 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def new
- @snippet = @project.snippets.build
+ @snippet = @noteable = @project.snippets.build
end
def create
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 280fe12cc7c..e580487a2c6 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -34,6 +34,11 @@ class Projects::TagsController < Projects::ApplicationController
def destroy
DeleteTagService.new(project, current_user).execute(params[:id])
- redirect_to namespace_project_tags_path(@project.namespace, @project)
+ respond_to do |format|
+ format.html do
+ redirect_to namespace_project_tags_path(@project.namespace, @project)
+ end
+ format.js
+ end
end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index 30adfad1daa..92359745cec 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -1,5 +1,5 @@
class Projects::TriggersController < Projects::ApplicationController
- before_action :authorize_admin_project!
+ before_action :authorize_admin_build!
layout 'project_settings'
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 10efafea9db..00234654578 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -1,5 +1,5 @@
class Projects::VariablesController < Projects::ApplicationController
- before_action :authorize_admin_project!
+ before_action :authorize_admin_build!
layout 'project_settings'
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3004722bce0..c9930480770 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,14 +1,13 @@
class ProjectsController < ApplicationController
include ExtractsPath
- prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [: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?
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update]
+ before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping]
before_action :event_filter, only: [:show, :activity]
layout :determine_layout
@@ -93,6 +92,10 @@ class ProjectsController < ApplicationController
return
end
+ if @project.pending_delete?
+ flash[:alert] = "Project queued for delete."
+ end
+
respond_to do |format|
format.html do
if @project.repository_exists?
@@ -120,8 +123,8 @@ class ProjectsController < ApplicationController
def destroy
return access_denied! unless can?(current_user, :remove_project, @project)
- ::Projects::DestroyService.new(@project, current_user, {}).execute
- flash[:alert] = "Project '#{@project.name}' was deleted."
+ ::Projects::DestroyService.new(@project, current_user, {}).pending_delete!
+ flash[:alert] = "Project '#{@project.name}' will be deleted."
redirect_to dashboard_projects_path
rescue Projects::DestroyService::DestroyError => ex
@@ -131,7 +134,7 @@ 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 = {
@@ -166,6 +169,20 @@ class ProjectsController < ApplicationController
end
end
+ def housekeeping
+ ::Projects::HousekeepingService.new(@project).execute
+
+ redirect_to(
+ project_path(@project),
+ notice: "Housekeeping successfully started"
+ )
+ rescue ::Projects::HousekeepingService::LeaseTaken => ex
+ redirect_to(
+ edit_project_path(@project),
+ alert: ex.to_s
+ )
+ end
+
def toggle_star
current_user.toggle_star(@project)
@project.reload
@@ -214,6 +231,7 @@ class ProjectsController < ApplicationController
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
+ :public_builds,
)
end
@@ -222,22 +240,12 @@ class ProjectsController < ApplicationController
Emoji.emojis.map do |name, emoji|
{
name: name,
- path: view_context.image_url("emoji/#{emoji["unicode"]}.png")
+ path: view_context.image_url("#{emoji["unicode"]}.png")
}
end
end
end
- def render_go_import
- return unless params["go-get"] == "1"
-
- @namespace = params[:namespace_id]
- @id = params[:project_id] || params[:id]
- @id = @id.gsub(/\.git\Z/, "")
-
- render "go_import", layout: false
- end
-
def repo_exists?
project.repository_exists? && !project.empty_repo?
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 9bb42ec86b3..e42d2d73947 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,4 +1,6 @@
class SearchController < ApplicationController
+ skip_before_action :authenticate_user!, :reject_blocked
+
include SearchHelper
layout 'search'
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
new file mode 100644
index 00000000000..7271c933b9b
--- /dev/null
+++ b/app/controllers/sent_notifications_controller.rb
@@ -0,0 +1,25 @@
+class SentNotificationsController < ApplicationController
+ skip_before_action :authenticate_user!
+
+ def unsubscribe
+ @sent_notification = SentNotification.for(params[:id])
+ return render_404 unless @sent_notification && @sent_notification.unsubscribable?
+
+ noteable = @sent_notification.noteable
+ noteable.unsubscribe(@sent_notification.recipient)
+
+ flash[:notice] = "You have been unsubscribed from this thread."
+ if current_user
+ case noteable
+ when Issue
+ redirect_to issue_path(noteable)
+ when MergeRequest
+ redirect_to merge_request_path(noteable)
+ else
+ redirect_to root_path
+ end
+ else
+ redirect_to new_user_session_path
+ end
+ end
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 825f85199be..65677a3dd3c 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -2,8 +2,12 @@ class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
include Recaptcha::ClientHelper
+ skip_before_action :check_2fa_requirement, only: [:destroy]
+
+ prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
+
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
@@ -31,6 +35,22 @@ class SessionsController < Devise::SessionsController
private
+ # Handle an "initial setup" state, where there's only one user, it's an admin,
+ # and they require a password change.
+ def check_initial_setup
+ return unless User.count == 1
+
+ user = User.admins.last
+
+ return unless user && user.require_password?
+
+ token = user.generate_reset_token
+ user.save
+
+ redirect_to edit_user_password_path(reset_password_token: token),
+ notice: "Please create a password for your new account."
+ end
+
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt)
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 868b05929d7..509f4f412ca 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -55,14 +55,15 @@ class UploadsController < ApplicationController
"user" => User,
"project" => Project,
"note" => Note,
- "group" => Group
+ "group" => Group,
+ "appearance" => Appearance
}
upload_models[params[:model]]
end
def upload_mount
- upload_mounts = %w(avatar attachment file)
+ upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as])
params[:mounted_as]
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 30cb869eb2a..e10c633690f 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,12 +3,6 @@ class UsersController < ApplicationController
before_action :set_user
def show
- @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
-
- @projects = PersonalProjectsFinder.new(@user).execute(current_user)
-
- @groups = JoinedGroupsFinder.new(@user).execute(current_user)
-
respond_to do |format|
format.html
@@ -24,6 +18,45 @@ class UsersController < ApplicationController
end
end
+ def groups
+ load_groups
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/groups/_list", groups: @groups)
+ }
+ end
+ end
+ end
+
+ def projects
+ load_projects
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/projects/_list", projects: @projects, remote: true)
+ }
+ end
+ end
+ end
+
+ def contributed
+ load_contributed_projects
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/projects/_list", projects: @contributed_projects)
+ }
+ end
+ end
+ end
+
def calendar
calendar = contributions_calendar
@timestamps = calendar.timestamps
@@ -34,12 +67,8 @@ class UsersController < ApplicationController
end
def calendar_activities
- @calendar_date = Date.parse(params[:date]) rescue nil
- @events = []
-
- if @calendar_date
- @events = contributions_calendar.events_by_date(@calendar_date)
- end
+ @calendar_date = Date.parse(params[:date]) rescue Date.today
+ @events = contributions_calendar.events_by_date(@calendar_date)
render 'calendar_activities', layout: false
end
@@ -56,7 +85,7 @@ class UsersController < ApplicationController
def contributions_calendar
@contributions_calendar ||= Gitlab::ContributionsCalendar.
- new(contributed_projects.reject(&:forked?), @user)
+ new(contributed_projects, @user)
end
def load_events
@@ -68,6 +97,20 @@ class UsersController < ApplicationController
limit_recent(20, params[:offset])
end
+ def load_projects
+ @projects =
+ PersonalProjectsFinder.new(@user).execute(current_user)
+ .page(params[:page]).per(PER_PAGE)
+ end
+
+ def load_contributed_projects
+ @contributed_projects = contributed_projects.joined(@user)
+ end
+
+ def load_groups
+ @groups = @user.groups.order_id_desc
+ end
+
def projects_for_current_user
ProjectsFinder.new.execute(current_user)
end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
deleted file mode 100644
index 91cb0f228f0..00000000000
--- a/app/finders/groups_finder.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-class GroupsFinder
- # Finds the groups available to the given user.
- #
- # current_user - The user to find the groups for.
- #
- # Returns an ActiveRecord::Relation.
- def execute(current_user = nil)
- if current_user
- relation = groups_visible_to_user(current_user)
- else
- relation = public_groups
- end
-
- relation.order_id_desc
- end
-
- private
-
- # This method returns the groups "current_user" can see.
- def groups_visible_to_user(current_user)
- base = groups_for_projects(public_and_internal_projects)
-
- union = Gitlab::SQL::Union.
- new([base.select(:id), current_user.authorized_groups.select(:id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})")
- end
-
- def public_groups
- groups_for_projects(public_projects)
- end
-
- def groups_for_projects(projects)
- Group.public_and_given_groups(projects.select(:namespace_id))
- end
-
- def public_projects
- Project.unscoped.public_only
- end
-
- def public_and_internal_projects
- Project.unscoped.public_and_internal_only
- end
-end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 3d5e8b6fbe7..19e8c7a92be 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -79,9 +79,10 @@ class IssuableFinder
if project?
@projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
- @projects = current_user.authorized_projects
+ @projects = current_user.authorized_projects.reorder(nil)
else
- @projects = ProjectsFinder.new.execute(current_user)
+ @projects = ProjectsFinder.new.execute(current_user, group: group).
+ reorder(nil)
end
end
@@ -118,6 +119,20 @@ class IssuableFinder
labels? && params[:label_name] == Label::None.title
end
+ def labels
+ return @labels if defined?(@labels)
+
+ if labels? && !filter_by_no_label?
+ @labels = Label.where(title: label_names)
+
+ if projects
+ @labels = @labels.where(project: projects)
+ end
+ else
+ @labels = Label.none
+ end
+ end
+
def assignee?
params[:assignee_id].present?
end
@@ -229,10 +244,17 @@ class IssuableFinder
items
end
+ def filter_by_upcoming_milestone?
+ params[:milestone_title] == '#upcoming'
+ end
+
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
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 })
else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
@@ -248,13 +270,9 @@ class IssuableFinder
def by_label(items)
if labels?
if filter_by_no_label?
- items = items.
- joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id").
- where(label_links: { id: nil })
+ items = items.without_label
else
- label_names = params[:label_name].split(",")
-
- items = items.joins(:labels).where(labels: { title: label_names })
+ items = items.with_label(label_names)
if projects
items = items.where(labels: { project_id: projects })
@@ -265,6 +283,10 @@ class IssuableFinder
items
end
+ def label_names
+ params[:label_name].split(',')
+ end
+
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
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
deleted file mode 100644
index e7523136fea..00000000000
--- a/app/finders/joined_groups_finder.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# Class for finding the groups a user is a member of.
-class JoinedGroupsFinder
- def initialize(user = nil)
- @user = user
- end
-
- # Finds the groups of the source user, optionally limited to those visible to
- # the current user.
- #
- # current_user - If given the groups of "@user" will only include the groups
- # "current_user" can also see.
- #
- # Returns an ActiveRecord::Relation.
- def execute(current_user = nil)
- if current_user
- relation = groups_visible_to_user(current_user)
- else
- relation = public_groups
- end
-
- relation.order_id_desc
- end
-
- private
-
- # Returns the groups the user in "current_user" can see.
- #
- # This list includes all public/internal projects as well as the projects of
- # "@user" that "current_user" also has access to.
- def groups_visible_to_user(current_user)
- base = @user.authorized_groups.visible_to_user(current_user)
- extra = public_and_internal_groups
- union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})")
- end
-
- def public_groups
- groups_for_projects(@user.authorized_projects.public_only)
- end
-
- def public_and_internal_groups
- groups_for_projects(@user.authorized_projects.public_and_internal_only)
- end
-
- def groups_for_projects(projects)
- @user.groups.public_and_given_groups(projects.select(:namespace_id))
- end
-end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 3b4e0362e04..3a5fc5b5907 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -40,21 +40,26 @@ class ProjectsFinder
private
def group_projects(current_user, group)
- if current_user
- [
- group_projects_for_user(current_user, group),
- group.projects.public_and_internal_only
- ]
+ 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
- [group.projects.public_only]
+ user_group_projects << group.projects.public_and_internal_only
end
end
def all_projects(current_user)
- if current_user
- [current_user.authorized_projects, public_and_internal_projects]
+ return [public_projects] unless current_user
+
+ if current_user.external?
+ [current_user.authorized_projects, public_projects]
else
- [Project.public_only]
+ [current_user.authorized_projects, public_and_internal_projects]
end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 07b5759443b..a41172816b8 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -4,7 +4,7 @@ class SnippetsFinder
case filter
when :all then
- snippets(current_user).fresh.non_expired
+ snippets(current_user).fresh
when :by_user then
by_user(current_user, params[:user], params[:scope])
when :by_project
@@ -27,7 +27,7 @@ class SnippetsFinder
end
def by_user(current_user, user, scope)
- snippets = user.snippets.fresh.non_expired
+ snippets = user.snippets.fresh
return snippets.are_public unless current_user
@@ -48,7 +48,7 @@ class SnippetsFinder
end
def by_project(current_user, project)
- snippets = project.snippets.fresh.non_expired
+ snippets = project.snippets.fresh
if current_user
if project.team.member?(current_user.id)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
new file mode 100644
index 00000000000..3ba27c40504
--- /dev/null
+++ b/app/finders/todos_finder.rb
@@ -0,0 +1,129 @@
+# TodosFinder
+#
+# Used to filter Todos by set of params
+#
+# Arguments:
+# current_user - which user use
+# params:
+# action_id: integer
+# author_id: integer
+# project_id; integer
+# state: 'pending' or 'done'
+# type: 'Issue' or 'MergeRequest'
+#
+
+class TodosFinder
+ NONE = '0'
+
+ attr_accessor :current_user, :params
+
+ def initialize(current_user, params)
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ items = current_user.todos
+ items = by_action_id(items)
+ items = by_author(items)
+ items = by_project(items)
+ items = by_state(items)
+ items = by_type(items)
+
+ items
+ end
+
+ private
+
+ def action_id?
+ action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i)
+ end
+
+ def action_id
+ params[:action_id]
+ end
+
+ def author?
+ params[:author_id].present?
+ end
+
+ def author
+ return @author if defined?(@author)
+
+ @author =
+ if author? && params[:author_id] != NONE
+ User.find(params[:author_id])
+ else
+ nil
+ end
+ end
+
+ def project?
+ params[:project_id].present?
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ if project?
+ @project = Project.find(params[:project_id])
+
+ unless Ability.abilities.allowed?(current_user, :read_project, @project)
+ @project = nil
+ end
+ else
+ @project = nil
+ end
+
+ @project
+ end
+
+ def type?
+ type.present? && ['Issue', 'MergeRequest'].include?(type)
+ end
+
+ def type
+ params[:type]
+ end
+
+ def by_action_id(items)
+ if action_id?
+ items = items.where(action: action_id)
+ end
+
+ items
+ end
+
+ def by_author(items)
+ if author?
+ items = items.where(author_id: author.try(:id))
+ end
+
+ items
+ end
+
+ def by_project(items)
+ if project?
+ items = items.where(project: project)
+ end
+
+ items
+ end
+
+ def by_state(items)
+ case params[:state]
+ when 'done'
+ items.done
+ else
+ items.pending
+ end
+ end
+
+ def by_type(items)
+ if type?
+ items = items.where(target_type: type)
+ end
+
+ items
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index c5820bf4c50..e0abc3a2869 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,21 +1,33 @@
module AppearancesHelper
- def brand_item
- nil
- end
-
def brand_title
- 'GitLab Community Edition'
+ if brand_item && brand_item.title
+ brand_item.title
+ else
+ 'GitLab Community Edition'
+ end
end
def brand_image
- nil
+ if brand_item.logo?
+ image_tag brand_item.logo
+ else
+ nil
+ end
end
def brand_text
- nil
+ markdown(brand_item.description)
+ end
+
+ def brand_item
+ @appearance ||= Appearance.first
end
def brand_header_logo
- render 'shared/logo.svg'
+ if brand_item && brand_item.header_logo?
+ image_tag brand_item.header_logo
+ else
+ render 'shared/logo.svg'
+ end
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index f7f7a1a02d3..e6ceb213532 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
- user = User.find_by(email: user_or_email.downcase)
+ user = User.find_by_any_email(user_or_email.try(:downcase))
end
if user
@@ -118,12 +118,6 @@ module ApplicationHelper
grouped_options_for_select(options, @ref || @project.default_branch)
end
- def emoji_autocomplete_source
- # should be an array of strings
- # so to_s can be called, because it is sufficient and to_json is too slow
- Emoji.names.to_s
- end
-
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
@@ -169,22 +163,6 @@ module ApplicationHelper
Gitlab.config.extra
end
- def search_placeholder
- if @project && @project.persisted?
- 'Search in this project'
- elsif @snippet || @snippets || @show_snippets
- 'Search snippets'
- elsif @group && @group.persisted?
- 'Search in this group'
- else
- 'Search'
- end
- end
-
- def broadcast_message
- BroadcastMessage.current
- end
-
# Render a `time` element with Javascript-based relative date and tooltip
#
# time - Time object
@@ -204,9 +182,9 @@ module ApplicationHelper
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
- class: "#{html_class} js-timeago js-timeago-pending",
- datetime: time.getutc.iso8601,
- title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
+ class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
+ datetime: time.to_time.getutc.iso8601,
+ title: time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
unless skip_js
@@ -218,6 +196,22 @@ module ApplicationHelper
element
end
+ def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', include_author: false)
+ return if object.updated_at == object.created_at
+
+ content_tag :small, class: "edited-text" do
+ output = content_tag(:span, "Edited ")
+ output << time_ago_with_tooltip(object.updated_at, placement: placement, html_class: html_class)
+
+ if include_author && object.updated_by && object.updated_by != object.author
+ output << content_tag(:span, " by ")
+ output << link_to_member(object.project, object.updated_by, avatar: false, author_class: nil)
+ end
+
+ output
+ end
+ end
+
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Haml::Helpers.preserve(markdown(file_content))
@@ -228,8 +222,7 @@ module ApplicationHelper
file_content
end
else
- GitHub::Markup.render(file_name, file_content).
- force_encoding(file_content.encoding).html_safe
+ other_markup(file_name, file_content)
end
rescue RuntimeError
simple_format(file_content)
@@ -266,7 +259,7 @@ module ApplicationHelper
state: params[:state],
scope: params[:scope],
label_name: params[:label_name],
- milestone_id: params[:milestone_id],
+ milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
sort: params[:sort],
@@ -308,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 7d6b58ee21a..23693629a4c 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -23,6 +23,10 @@ module ApplicationSettingsHelper
current_application_settings.user_oauth_applications
end
+ def askimet_enabled?
+ current_application_settings.akismet_enabled?
+ end
+
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def restricted_level_checkboxes(help_block_id)
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 0cfc0565e84..b4f80fd9b3e 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,11 +1,15 @@
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
Gitlab.config.ldap.enabled
end
+ def omniauth_enabled?
+ Gitlab.config.omniauth.enabled
+ end
+
def provider_has_icon?(name)
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index d31d4cde08f..0f77b3b299a 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -1,21 +1,10 @@
module BlobHelper
- def highlight(blob_name, blob_content, nowrap: false, continue: false)
- @formatter ||= Rouge::Formatters::HTMLGitlab.new(
- nowrap: nowrap,
- cssclass: 'code highlight',
- lineanchors: true,
- lineanchorsid: 'LC'
- )
-
- begin
- @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
- result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe
- rescue
- @lexer = Rouge::Lexers::PlainText
- result = @formatter.format(@lexer.lex(blob_content)).html_safe
- end
+ def highlighter(blob_name, blob_content, nowrap: false)
+ Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap)
+ end
- result
+ def highlight(blob_name, blob_content, nowrap: false)
+ Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap)
end
def no_highlight_files
@@ -37,20 +26,19 @@ module BlobHelper
tree_join(ref, path),
link_opts)
- if !on_top_of_branch?
+ 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' }
- elsif can_edit_blob?(blob)
- link_to "Edit", edit_path, class: 'btn btn-small'
+ elsif can_edit_blob?(blob, project, ref)
+ link_to "Edit", edit_path, class: 'btn'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
- fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
- continue: continue_params)
+ fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- link_to "Edit", fork_path, class: 'btn btn-small', method: :post
+ link_to "Edit", fork_path, class: 'btn', method: :post
end
end
@@ -61,11 +49,11 @@ module BlobHelper
return unless blob
- if !on_top_of_branch?
+ 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' }
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' }
- elsif can_edit_blob?(blob)
+ 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)
continue_params = {
@@ -73,8 +61,7 @@ module BlobHelper
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
- fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
- continue: continue_params)
+ fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post
end
@@ -139,4 +126,51 @@ module BlobHelper
blob.size
end
end
+
+ # SVGs can contain malicious JavaScript; only include whitelisted
+ # elements and attributes. Note that this whitelist is by no means complete
+ # and may omit some elements.
+ def sanitize_svg(blob)
+ blob.data = Loofah.scrub_fragment(blob.data, :strip).to_xml
+ blob
+ end
+
+ # If we blindly set the 'real' content type when serving a Git blob we
+ # are enabling XSS attacks. An attacker could upload e.g. a Javascript
+ # file to a Git repository, trick the browser of a victim into
+ # downloading the blob, and then the 'application/javascript' content
+ # type would tell the browser to execute the attacker's Javascript. By
+ # overriding the content type and setting it to 'text/plain' (in the
+ # example of Javascript) we tell the browser of the victim not to
+ # execute untrusted data.
+ def safe_content_type(blob)
+ if blob.text?
+ 'text/plain; charset=utf-8'
+ elsif blob.image?
+ blob.content_type
+ else
+ 'application/octet-stream'
+ end
+ end
+
+ def cached_blob?
+ stale = stale?(etag: @blob.id) # The #stale? method sets cache headers.
+
+ # Because we are opionated we set the cache headers ourselves.
+ response.cache_control[:public] = @project.public?
+
+ if @ref && @commit && @ref == @commit.id
+ # This is a link to a commit by its commit SHA. That means that the blob
+ # is immutable. The only reason to invalidate the cache is if the commit
+ # was deleted or if the user lost access to the repository.
+ response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
+ else
+ # A branch or tag points at this blob. That means that the expected blob
+ # value may change over time.
+ response.cache_control[:max_age] = Blob::CACHE_TIME
+ end
+
+ response.etag = @blob.id
+ !stale
+ end
end
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 6484dca6b55..43a29c96bca 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -1,16 +1,38 @@
module BroadcastMessagesHelper
- def broadcast_styling(broadcast_message)
- styling = ''
+ def broadcast_message(message = BroadcastMessage.current)
+ return unless message.present?
+
+ content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
+ icon('bullhorn') << ' ' << render_broadcast_message(message.message)
+ end
+ end
+
+ def broadcast_message_style(broadcast_message)
+ style = ''
if broadcast_message.color.present?
- styling << "background-color: #{broadcast_message.color}"
- styling << '; ' if broadcast_message.font.present?
+ style << "background-color: #{broadcast_message.color}"
+ style << '; ' if broadcast_message.font.present?
end
if broadcast_message.font.present?
- styling << "color: #{broadcast_message.font}"
+ style << "color: #{broadcast_message.font}"
end
- styling
+ style
+ end
+
+ def broadcast_message_status(broadcast_message)
+ if broadcast_message.active?
+ 'Active'
+ elsif broadcast_message.ended?
+ 'Expired'
+ else
+ 'Pending'
+ end
+ end
+
+ def render_broadcast_message(message)
+ Banzai.render(message, pipeline: :broadcast_message).html_safe
end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index ec0e3f409c1..d6c05843743 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -17,7 +17,7 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
- class: 'btn btn-xs btn-clipboard',
+ class: 'btn btn-clipboard',
data: data,
type: :button
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index d8bee21c82e..8b1575d5e0c 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -12,9 +12,13 @@ module CiStatusHelper
ci_label_for_status(ci_commit.status)
end
- def ci_status_with_icon(status)
- content_tag :span, class: "ci-status ci-#{status}" do
- ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ def ci_status_with_icon(status, target = nil)
+ content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ klass = "ci-status ci-#{status}"
+ if target
+ link_to content, target, class: klass
+ else
+ content_tag :span, content, class: klass
end
end
@@ -42,12 +46,12 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
- def render_ci_status(ci_commit)
+ def render_ci_status(ci_commit, tooltip_placement: 'auto left')
link_to ci_status_icon(ci_commit),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}",
- data: { toggle: 'tooltip', placement: 'left' }
+ data: { toggle: 'tooltip', placement: tooltip_placement }
end
def no_runners_for_project?(project)
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 590d20ac7b3..f994c9e6170 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -123,6 +123,37 @@ module CommitsHelper
)
end
+ def revert_commit_link(commit, continue_to_path, btn_class: nil)
+ return unless current_user
+
+ tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request"
+
+ if can_collaborate_with_project?
+ content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do
+ link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
+ end
+ elsif can?(current_user, :fork_project, @project)
+ continue_params = {
+ to: continue_to_path,
+ notice: edit_in_new_fork_notice + ' Try to revert this commit again.',
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(@project.namespace, @project,
+ namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
+ end
+ end
+
+ def revert_commit_type(commit)
+ if commit.merged_merge_request
+ 'merge request'
+ else
+ 'commit'
+ end
+ end
+
protected
# Private: Returns a link to a person. If the person has a matching user and
@@ -152,7 +183,7 @@ module CommitsHelper
options = {
class: "commit-#{options[:source]}-link has_tooltip",
- data: { :'original-title' => sanitize(source_email) }
+ data: { 'original-title'.to_sym => sanitize(source_email) }
}
if user.nil?
@@ -166,7 +197,7 @@ module CommitsHelper
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff.new_path)),
- class: 'btn btn-small view-file js-view-file'
+ class: 'btn view-file js-view-file'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
@@ -180,4 +211,15 @@ module CommitsHelper
def clean(string)
Sanitize.clean(string, remove_contents: true)
end
+
+ def limited_commits(commits)
+ if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ [
+ commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE),
+ commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE
+ ]
+ else
+ [commits, 0]
+ end
+ end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 24134310fc5..ff32e834499 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -1,106 +1,37 @@
module DiffHelper
+ def mark_inline_diffs(old_line, new_line)
+ old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs
+
+ marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs)
+ marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs)
+
+ [marked_old_line, marked_new_line]
+ end
+
def diff_view
params[:view] == 'parallel' ? 'parallel' : 'inline'
end
- def allowed_diff_size
- if diff_hard_limit_enabled?
- Commit::DIFF_HARD_LIMIT_FILES
- else
- Commit::DIFF_SAFE_FILES
- end
+ def diff_hard_limit_enabled?
+ params[:force_show_diff].present?
end
- def allowed_diff_lines
+ def diff_options
+ options = { ignore_whitespace_change: params[:w] == '1' }
if diff_hard_limit_enabled?
- Commit::DIFF_HARD_LIMIT_LINES
- else
- Commit::DIFF_SAFE_LINES
+ options.merge!(Commit.max_diff_options)
end
+ options
end
- def safe_diff_files(diffs)
- lines = 0
- safe_files = []
- diffs.first(allowed_diff_size).each do |diff|
- lines += diff.diff.lines.count
- break if lines > allowed_diff_lines
- safe_files << Gitlab::Diff::File.new(diff)
- end
- safe_files
- end
-
- def diff_hard_limit_enabled?
- # Enabling hard limit allows user to see more diff information
- if params[:force_show_diff].present?
- true
- else
- false
- end
+ def safe_diff_files(diffs, diff_refs)
+ diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs) }
end
def generate_line_code(file_path, line)
Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
end
- def parallel_diff(diff_file, index)
- lines = []
- skip_next = false
-
- # Building array of lines
- #
- # [
- # left_type, left_line_number, left_line_content, left_line_code,
- # right_line_type, right_line_number, right_line_content, right_line_code
- # ]
- #
- diff_file.diff_lines.each do |line|
-
- full_line = line.text
- type = line.type
- line_code = generate_line_code(diff_file.file_path, line)
- line_new = line.new_pos
- line_old = line.old_pos
-
- next_line = diff_file.next_line(line.index)
-
- if next_line
- next_line_code = generate_line_code(diff_file.file_path, next_line)
- next_type = next_line.type
- next_line = next_line.text
- end
-
- if type == 'match' || type.nil?
- # line in the right panel is the same as in the left one
- line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code]
- lines.push(line)
- elsif type == 'old'
- if next_type == 'new'
- # Left side has text removed, right side has text added
- line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code]
- lines.push(line)
- skip_next = true
- elsif next_type == 'old' || next_type.nil?
- # Left side has text removed, right side doesn't have any change
- # No next line code, no new line number, no new line text
- line = [type, line_old, full_line, line_code, next_type, nil, "&nbsp;", nil]
- lines.push(line)
- end
- elsif type == 'new'
- if skip_next
- # Change has been already included in previous line so no need to do it again
- skip_next = false
- next
- else
- # Change is only on the right side, left side has no change
- line = [nil, nil, "&nbsp;", line_code, type, line_new, full_line, line_code]
- lines.push(line)
- end
- end
- end
- lines
- end
-
def unfold_bottom_class(bottom)
(bottom) ? 'js-unfold-bottom' : ''
end
@@ -111,14 +42,14 @@ module DiffHelper
def diff_line_content(line)
if line.blank?
- " &nbsp;"
+ " &nbsp;".html_safe
else
line
end
end
def line_comments
- @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code)
+ @line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code)
end
def organize_comments(type_left, type_right, line_code_left, line_code_right)
@@ -160,8 +91,7 @@ module DiffHelper
def commit_for_diff(diff)
if diff.deleted_file
- first_commit = @first_commit || @commit
- first_commit.parent || @first_commit
+ @base_commit || @commit.parent || @commit
else
@commit
end
@@ -187,7 +117,7 @@ module DiffHelper
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
- link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do
+ link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn'), data: { view_type: name } do
title
end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
new file mode 100644
index 00000000000..74f326e0b83
--- /dev/null
+++ b/app/helpers/dropdowns_helper.rb
@@ -0,0 +1,100 @@
+module DropdownsHelper
+ def dropdown_tag(toggle_text, options: {}, &block)
+ content_tag :div, class: "dropdown" do
+ data_attr = { toggle: "dropdown" }
+
+ if options.has_key?(:data)
+ data_attr = options[:data].merge(data_attr)
+ end
+
+ dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
+
+ dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
+ output = ""
+
+ if options.has_key?(:title)
+ output << dropdown_title(options[:title])
+ end
+
+ if options.has_key?(:filter)
+ output << dropdown_filter(options[:placeholder])
+ end
+
+ output << content_tag(:div, class: "dropdown-content") do
+ capture(&block) if block && !options.has_key?(:footer_content)
+ end
+
+ if block && options.has_key?(:footer_content)
+ output << content_tag(:div, class: "dropdown-footer") do
+ capture(&block)
+ end
+ end
+
+ output << dropdown_loading
+
+ output.html_safe
+ end
+
+ dropdown_output.html_safe
+ end
+ end
+
+ def dropdown_toggle(toggle_text, data_attr, options)
+ content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
+ output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
+ output << icon('chevron-down')
+ output.html_safe
+ end
+ end
+
+ def dropdown_title(title, back: false)
+ content_tag :div, class: "dropdown-title" do
+ title_output = ""
+
+ if back
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
+ icon('arrow-left')
+ end
+ end
+
+ 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')
+ end
+
+ title_output.html_safe
+ end
+ end
+
+ 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.html_safe
+ end
+ end
+
+ def dropdown_content(&block)
+ content_tag(:div, class: "dropdown-content") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_footer(&block)
+ content_tag(:div, class: "dropdown-footer") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_loading
+ content_tag :div, class: "dropdown-loading" do
+ icon('spinner spin')
+ end
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index dde83ff36b5..a67a6b208e2 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -3,7 +3,7 @@ module EventsHelper
author = event.author
if author
- link_to author.name, user_path(author.username)
+ link_to author.name, user_path(author.username), title: h(author.name)
else
event.author_name
end
@@ -27,13 +27,15 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
- class: "event-filter-link btn btn-default #{active}",
+ class: "event-filter-link",
id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}",
}
- link_to request.path, link_opts do
- content_tag(:span, ' ' + tooltip)
+ content_tag :li, class: active do
+ link_to request.path, link_opts do
+ content_tag(:span, ' ' + tooltip)
+ end
end
end
@@ -157,7 +159,7 @@ module EventsHelper
link_to(
namespace_project_commit_path(event.project.namespace, event.project,
event.note_commit_id,
- anchor: dom_id(event.target)),
+ anchor: dom_id(event.target), title: h(event.target_title)),
class: "commit_short_id"
) do
"#{event.note_target_type} #{event.note_short_commit_id}"
@@ -165,12 +167,12 @@ module EventsHelper
elsif event.note_project_snippet?
link_to(namespace_project_snippet_path(event.project.namespace,
event.project,
- event.note_target)) do
- "#{event.note_target_type} ##{truncate event.note_target_id}"
+ event.note_target), title: h(event.project.name)) do
+ "#{event.note_target_type} #{truncate event.note_target.to_reference}"
end
else
link_to event_note_target_path(event) do
- "#{event.note_target_type} ##{truncate event.note_target_iid}"
+ "#{event.note_target_type} #{truncate event.note_target.to_reference}"
end
end
else
@@ -192,7 +194,7 @@ module EventsHelper
end
def event_to_atom(xml, event)
- if event.proper?
+ if event.proper?(current_user)
xml.entry do
event_link = event_feed_url(event)
event_title = event_feed_title(event)
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index 0d291f9a87e..337b0aacbb5 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -1,5 +1,5 @@
module ExploreHelper
- def explore_projects_filter_path(options={})
+ def filter_projects_path(options={})
exist_opts = {
sort: params[:sort],
scope: params[:scope],
@@ -9,9 +9,12 @@ module ExploreHelper
}
options = exist_opts.merge(options)
-
- path = explore_projects_path
+ path = request.path
path << "?#{options.to_param}"
path
end
+
+ def explore_controller?
+ controller.class.name.split("::").first == "Explore"
+ end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index ca41657cec1..2f760af02fd 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -50,6 +50,8 @@ module GitlabMarkdownHelper
context[:project] ||= @project
+ text = Banzai.pre_process(text, context)
+
html = Banzai.render(text, context)
context.merge!(
@@ -78,6 +80,21 @@ module GitlabMarkdownHelper
)
end
+ def other_markup(file_name, text)
+ Gitlab::OtherMarkup.render(
+ file_name,
+ text,
+ project: @project,
+ current_user: (current_user if defined?(current_user)),
+
+ # RelativeLinkFilter
+ project_wiki: @project_wiki,
+ requested_path: @path,
+ ref: @ref,
+ commit: @commit
+ )
+ end
+
# Return the first line of +text+, up to +max_chars+, after parsing the line
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
@@ -91,7 +108,7 @@ module GitlabMarkdownHelper
def render_wiki_content(wiki_page)
case wiki_page.format
when :markdown
- markdown(wiki_page.content)
+ markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki)
when :asciidoc
asciidoc(wiki_page.content)
else
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 5724d3aabec..ab3ef454e1c 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -7,7 +7,16 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls.
def icon(names, options = {})
- fa_icon(names, options)
+ options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
+ end
+
+ def audit_icon(names, options = {})
+ case names
+ when "standard"
+ names = "key"
+ end
+
+ options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end
def spinner(text = nil, visible = false)
@@ -37,7 +46,7 @@ module IconsHelper
else # Gitlab::VisibilityLevel::PUBLIC
'globe'
end
-
+
name << " fw" if fw
icon(name)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
new file mode 100644
index 00000000000..81df2094392
--- /dev/null
+++ b/app/helpers/issuables_helper.rb
@@ -0,0 +1,58 @@
+module IssuablesHelper
+
+ def sidebar_gutter_toggle_icon
+ sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right')
+ end
+
+ def sidebar_gutter_collapsed_class
+ "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
+ end
+
+ def issuables_count(issuable)
+ base_issuable_scope(issuable).maximum(:iid)
+ end
+
+ def next_issuable_for(issuable)
+ base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
+ end
+
+ def prev_issuable_for(issuable)
+ base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
+ end
+
+ def user_dropdown_label(user_id, default_label)
+ return "Unassigned" if user_id == "0"
+
+ if @project
+ member = @project.team.find_member(user_id)
+ user = member.user if member
+ else
+ user = User.find_by(id: user_id)
+ end
+
+ if user
+ user.name
+ else
+ default_label
+ end
+ end
+
+ private
+
+ def sidebar_gutter_collapsed?
+ cookies[:collapsed_gutter] == 'true'
+ end
+
+ def base_issuable_scope(issuable)
+ issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable))
+ end
+
+ def issuable_state_scope(issuable)
+ if issuable.respond_to?(:merged?) && issuable.merged?
+ :merged
+ else
+ issuable.open? ? :opened : :closed
+ end
+ end
+
+end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 80e2741b09a..e00d3204027 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -44,14 +44,14 @@ module IssuesHelper
end
def bulk_update_milestone_options
- milestones = project_active_milestones.to_a
+ milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
end
def milestone_options(object)
- milestones = object.project.milestones.active.to_a
+ milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
@@ -69,7 +69,7 @@ module IssuesHelper
end
end
- def issue_button_visibility(issue, closed)
+ def issue_button_visibility(issue, closed)
return 'hidden' if issue.closed? == closed
end
@@ -80,7 +80,7 @@ module IssuesHelper
xml.link href: namespace_project_issue_url(issue.project.namespace,
issue.project, issue)
xml.title truncate(issue.title, length: 80)
- xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.updated issue.created_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
xml.author do |author|
xml.name issue.author_name
@@ -98,14 +98,21 @@ 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)
+ unicode ||= Emoji.emoji_filename(name) rescue ""
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
- "data-emoji" => name,
- "data-aliases" => aliases.join(" "),
- "data-unicode-name" => unicode
+ title: name,
+ data: {
+ aliases: aliases.join(' '),
+ emoji: name,
+ unicode_name: unicode
+ }
end
def emoji_author_list(notes, current_user)
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index a2c3d4d2f32..4455dcd0e20 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -7,6 +7,8 @@ module LabelsHelper
# project - Project object which will be used as the context for the label's
# link. If omitted, defaults to `@project`, or the label's own
# project.
+ # type - The type of item the link will point to (:issue or
+ # :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the
# body of the link element. If omitted, defaults to
# `render_colored_label`.
@@ -23,14 +25,19 @@ module LabelsHelper
# # Force the generated link to use a provided project
# link_to_label(label, project: Project.last)
#
+ # # Force the generated link to point to merge requests instead of issues
+ # link_to_label(label, type: :merge_request)
+ #
# # Customize link body with a block
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, project: nil, &block)
+ def link_to_label(label, project: nil, type: :issue, &block)
project ||= @project || label.project
- link = namespace_project_issues_path(project.namespace, project,
- label_name: label.name)
+ link = send("namespace_project_#{type.to_s.pluralize}_path",
+ project.namespace,
+ project,
+ label_name: label.name)
if block_given?
link_to link, &block
@@ -43,19 +50,25 @@ module LabelsHelper
@project.labels.pluck(:title)
end
- def render_colored_label(label)
+ def render_colored_label(label, label_suffix = '')
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}">) +
- escape_once(label.name) + '</span>'
+ %(style="background-color: #{label_color}; color: #{text_color}">) +
+ %(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe
end
+ def render_colored_cross_project_label(label)
+ label_suffix = label.project.name_with_namespace
+ label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
+ render_colored_label(label, label_suffix)
+ end
+
def suggested_colors
[
'#0033CC',
@@ -83,7 +96,11 @@ module LabelsHelper
end
def text_color_for_bg(bg_color)
- r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
+ if bg_color.length == 4
+ r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex }
+ else
+ r, g, b = bg_color[1, 7].scan(/.{2}/).map(&:hex)
+ end
if (r + g + b) > 500
'#333333'
@@ -107,6 +124,15 @@ module LabelsHelper
options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end
+ def label_subscription_status(label)
+ label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ end
+
+ def label_subscription_toggle_button_text(label)
+ label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
- module_function :render_colored_label, :text_color_for_bg, :escape_once
+ module_function :render_colored_label, :render_colored_cross_project_label,
+ :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index a42cbcff182..92ed0891e92 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -9,10 +9,36 @@ module MilestonesHelper
end
end
+ def milestones_label_path(opts = {})
+ if @project
+ namespace_project_issues_path(@project.namespace, @project, opts)
+ elsif @group
+ issues_group_path(@group, opts)
+ else
+ issues_dashboard_path(opts)
+ end
+ end
+
+ def milestones_browse_issuables_path(milestone, type:)
+ opts = { milestone_title: milestone.title }
+
+ if @project
+ polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
+ elsif @group
+ polymorphic_url([type, @group], opts)
+ else
+ polymorphic_url([type, :dashboard], opts)
+ end
+ end
+
+ def milestone_issues_by_label_count(milestone, label, state:)
+ milestone.issues.with_label(label.title).send(state).size
+ end
+
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
@@ -33,7 +59,18 @@ module MilestonesHelper
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])
end
+
+ def milestone_remaining_days(milestone)
+ if milestone.expired?
+ content_tag(:strong, 'expired')
+ elsif milestone.due_date
+ days = milestone.remaining_days
+ content = content_tag(:strong, days)
+ content << " #{'day'.pluralize(days)} remaining"
+ end
+ end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index e6fb8670e57..5d86bd490a8 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -19,6 +19,20 @@ module NavHelper
end
end
+ def page_gutter_class
+ if current_path?('merge_requests#show') ||
+ current_path?('merge_requests#diffs') ||
+ current_path?('merge_requests#commits') ||
+ current_path?('merge_requests#builds') ||
+ current_path?('issues#show')
+ if cookies[:collapsed_gutter] == 'true'
+ "page-gutter right-sidebar-collapsed"
+ else
+ "page-gutter right-sidebar-expanded"
+ end
+ end
+ end
+
def nav_header_class
if nav_menu_collapsed?
"header-collapsed"
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 5f0c921413a..53c543c28c5 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -67,7 +67,7 @@ module NotesHelper
line_type: line_type
}
- button_tag class: 'btn reply-btn js-discussion-reply-button',
+ button_tag class: 'btn btn-nr reply-btn js-discussion-reply-button',
data: data, title: 'Add a reply' do
link_text = icon('comment')
link_text << ' Reply'
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 791cb9e50bd..82f805fa444 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -27,35 +27,20 @@ module PageLayoutHelper
#
# Returns an HTML-safe String.
def page_description(description = nil)
- @page_description ||= page_description_default
-
if description.present?
@page_description = description.squish
- else
+ elsif @page_description.present?
sanitize(@page_description, tags: []).truncate_words(30)
end
end
- # Default value for page_description when one hasn't been defined manually by
- # a view
- def page_description_default
- if @project
- @project.description || brand_title
- else
- brand_title
- end
- end
-
def page_image
default = image_url('gitlab_logo.png')
- if @project
- @project.avatar_url || default
- elsif @user
- avatar_icon(@user)
- else
- default
- end
+ subject = @project || @user || @group
+
+ image = subject.avatar_url if subject.present?
+ image || default
end
# Define or get attributes to be used as Twitter card metadata
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 77ba612548a..b5acb80b720 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -8,7 +8,7 @@ module ProjectsHelper
end
def link_to_project(project)
- link_to [project.namespace.becomes(Namespace), project] do
+ link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
if project.namespace
@@ -20,6 +20,12 @@ module ProjectsHelper
end
end
+ def link_to_member_avatar(author, opts = {})
+ default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
+ opts = default_opts.merge(opts)
+ 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 = {})
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
@@ -32,15 +38,19 @@ module ProjectsHelper
author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
# Build name span tag
- author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
+ if opts[:by_username]
+ author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name]
+ else
+ author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
+ end
author_html = author_html.html_safe
if opts[:name]
- link_to(author_html, user_path(author), class: "author_link").html_safe
+ 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' => 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
@@ -53,14 +63,23 @@ module ProjectsHelper
link_to(simple_sanitize(owner.name), user_path(owner))
end
- project_link = link_to(simple_sanitize(project.name), project_path(project))
+ project_link = link_to project_path(project), { class: "project-item-select-holder" } do
+ link_output = simple_sanitize(project.name)
+
+ if current_user
+ link_output += project_select_tag :project_path,
+ class: "project-item-select js-projects-dropdown",
+ data: { include_groups: false, order_by: 'last_activity_at' }
+ end
+
+ link_output
+ end
+ project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user
full_title = namespace_link + ' / ' + project_link
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
- content_tag :span do
- full_title
- end
+ full_title
end
def remove_project_message(project)
@@ -83,10 +102,6 @@ module ProjectsHelper
project_nav_tabs.include? name
end
- def project_active_milestones
- @project.milestones.active.order("due_date, title ASC")
- end
-
def project_for_deploy_key(deploy_key)
if deploy_key.projects.include?(@project)
@project
@@ -116,7 +131,7 @@ module ProjectsHelper
private
def get_project_nav_tabs(project, current_user)
- nav_tabs = [:home]
+ nav_tabs = [:home, :forks]
if !project.empty_repo? && can?(current_user, :download_code, project)
nav_tabs << [:files, :commits, :network, :graphs]
@@ -126,7 +141,7 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
- if project.builds_enabled? && can?(current_user, :read_build, project)
+ if can?(current_user, :read_build, project)
nav_tabs << :builds
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index a6ee6880247..494dad0b41e 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -40,7 +40,7 @@ module SearchHelper
{ 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: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") },
+ { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") },
]
end
@@ -70,7 +70,7 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
- GroupsFinder.new.execute(current_user).search(term).limit(limit).map do |group|
+ current_user.authorized_groups.search(term).limit(limit).map do |group|
{
label: "group: #{search_result_sanitize(group.name)}",
url: group_path(group)
@@ -80,7 +80,7 @@ module SearchHelper
# Autocomplete results for the current user's projects
def projects_autocomplete(term, limit = 5)
- ProjectsFinder.new.execute(current_user).search_by_title(term).
+ 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)}",
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 906cb12cd48..0a5a8eb5aee 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -1,14 +1,4 @@
module SnippetsHelper
- def lifetime_select_options
- options = [
- ['forever', nil],
- ['1 day', "#{Date.current + 1.day}"],
- ['1 week', "#{Date.current + 1.week}"],
- ['1 month', "#{Date.current + 1.month}"]
- ]
- options_for_select(options)
- end
-
def reliable_snippet_path(snippet)
if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace,
@@ -17,4 +7,79 @@ module SnippetsHelper
snippet_path(snippet)
end
end
+
+ # Get an array of line numbers surrounding a matching
+ # line, bounded by min/max.
+ #
+ # @returns Array of line numbers
+ def bounded_line_numbers(line, min, max, surrounding_lines)
+ lower = line - surrounding_lines > min ? line - surrounding_lines : min
+ upper = line + surrounding_lines < max ? line + surrounding_lines : max
+ (lower..upper).to_a
+ end
+
+ # Returns a sorted set of lines to be included in a snippet preview.
+ # This ensures matching adjacent lines do not display duplicated
+ # surrounding code.
+ #
+ # @returns Array, unique and sorted.
+ def matching_lines(lined_content, surrounding_lines, query)
+ used_lines = []
+ lined_content.each_with_index do |line, line_number|
+ used_lines.concat bounded_line_numbers(
+ line_number,
+ 0,
+ lined_content.size,
+ surrounding_lines
+ ) if line.include?(query)
+ end
+
+ used_lines.uniq.sort
+ end
+
+ # 'Chunkify' entire snippet. Splits the snippet data into matching lines +
+ # surrounding_lines() worth of unmatching lines.
+ #
+ # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}}
+ def chunk_snippet(snippet, query, surrounding_lines = 3)
+ lined_content = snippet.content.split("\n")
+ used_lines = matching_lines(lined_content, surrounding_lines, query)
+
+ snippet_chunk = []
+ snippet_chunks = []
+ snippet_start_line = 0
+ last_line = -1
+
+ # Go through each used line, and add consecutive lines as a single chunk
+ # to the snippet chunk array.
+ used_lines.each do |line_number|
+ if last_line < 0
+ # Start a new chunk.
+ snippet_start_line = line_number
+ snippet_chunk << lined_content[line_number]
+ elsif last_line == line_number - 1
+ # Consecutive line, continue chunk.
+ snippet_chunk << lined_content[line_number]
+ else
+ # Non-consecutive line, add chunk to chunk array.
+ snippet_chunks << {
+ data: snippet_chunk.join("\n"),
+ start_line: snippet_start_line + 1
+ }
+
+ # Start a new chunk.
+ snippet_chunk = [lined_content[line_number]]
+ snippet_start_line = line_number
+ end
+ last_line = line_number
+ end
+ # Add final chunk to chunk array
+ snippet_chunks << {
+ data: snippet_chunk.join("\n"),
+ start_line: snippet_start_line + 1
+ }
+
+ # Return snippet with chunk array
+ { snippet_object: snippet, snippet_chunks: snippet_chunks }
+ end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index bb12d43f397..2f2d2721d6d 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -11,6 +11,18 @@ module SortingHelper
sort_value_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
+ sort_value_downvotes => sort_title_downvotes,
+ sort_value_upvotes => sort_title_upvotes
+ }
+ end
+
+ def projects_sort_options_hash
+ {
+ sort_value_name => sort_title_name,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_oldest_created => sort_title_oldest_created,
}
end
@@ -19,7 +31,7 @@ module SortingHelper
end
def sort_title_recently_updated
- 'Recently updated'
+ 'Last updated'
end
def sort_title_oldest_created
@@ -27,7 +39,7 @@ module SortingHelper
end
def sort_title_recently_created
- 'Recently created'
+ 'Last created'
end
def sort_title_milestone_soon
@@ -54,6 +66,14 @@ module SortingHelper
'Oldest sign in'
end
+ def sort_title_downvotes
+ 'Least popular'
+ end
+
+ def sort_title_upvotes
+ 'Most popular'
+ end
+
def sort_value_oldest_updated
'updated_asc'
end
@@ -63,11 +83,11 @@ module SortingHelper
end
def sort_value_oldest_created
- 'created_asc'
+ 'id_asc'
end
def sort_value_recently_created
- 'created_desc'
+ 'id_desc'
end
def sort_value_milestone_soon
@@ -93,4 +113,12 @@ module SortingHelper
def sort_value_oldest_signin
'oldest_sign_in'
end
+
+ def sort_value_downvotes
+ 'downvotes_desc'
+ end
+
+ def sort_value_upvotes
+ 'upvotes_desc'
+ end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
new file mode 100644
index 00000000000..07ddc691d85
--- /dev/null
+++ b/app/helpers/todos_helper.rb
@@ -0,0 +1,87 @@
+module TodosHelper
+ def todos_pending_count
+ current_user.todos.pending.count
+ end
+
+ def todos_done_count
+ current_user.todos.done.count
+ end
+
+ def todo_action_name(todo)
+ case todo.action
+ when Todo::ASSIGNED then 'assigned you'
+ when Todo::MENTIONED then 'mentioned you on'
+ end
+ end
+
+ 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) }
+ 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)
+ end
+
+ def todos_filter_params
+ {
+ state: params[:state],
+ project_id: params[:project_id],
+ author_id: params[:author_id],
+ type: params[:type],
+ action_id: params[:action_id],
+ }
+ end
+
+ def todos_filter_path(options = {})
+ without = options.delete(:without)
+
+ options = todos_filter_params.merge(options)
+
+ if without.present?
+ without.each do |key|
+ options.delete(key)
+ end
+ end
+
+ path = request.path
+ path << "?#{options.to_param}"
+ path
+ end
+
+ def todo_actions_options
+ actions = [
+ OpenStruct.new(id: '', title: 'Any Action'),
+ OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'),
+ OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned')
+ ]
+
+ options_from_collection_for_select(actions, 'id', 'title', params[:action_id])
+ end
+
+ def todo_projects_options
+ projects = current_user.authorized_projects.sorted_by_activity.non_archived
+ projects = projects.includes(:namespace)
+
+ projects = projects.map do |project|
+ OpenStruct.new(id: project.id, title: project.name_with_namespace)
+ end
+
+ projects.unshift(OpenStruct.new(id: '', title: 'Any Project'))
+
+ options_from_collection_for_select(projects, 'id', 'title', params[:project_id])
+ end
+
+ def todo_types_options
+ types = [
+ OpenStruct.new(title: 'Any Type', name: ''),
+ OpenStruct.new(title: 'Issue', name: 'Issue'),
+ OpenStruct.new(title: 'Merge Request', name: 'MergeRequest')
+ ]
+
+ options_from_collection_for_select(types, 'name', 'title', params[:type])
+ end
+end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 2ad7c80dae0..4920ca5af6e 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -56,8 +56,7 @@ module TreeHelper
return false unless on_top_of_branch?(project, ref)
- can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project))
+ can_collaborate_with_project?(project)
end
def tree_edit_branch(project = @project, ref = @ref)
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
index f0c41f69a5c..d0ce827a595 100644
--- a/app/mailers/abuse_report_mailer.rb
+++ b/app/mailers/abuse_report_mailer.rb
@@ -2,11 +2,19 @@ class AbuseReportMailer < BaseMailer
include Gitlab::CurrentSettings
def notify(abuse_report_id)
+ return unless deliverable?
+
@abuse_report = AbuseReport.find(abuse_report_id)
mail(
- to: current_application_settings.admin_notification_email,
+ to: current_application_settings.admin_notification_email,
subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
)
end
+
+ private
+
+ def deliverable?
+ current_application_settings.admin_notification_email.present?
+ end
end
diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb
index 883f1c73ad4..76db31a4c45 100644
--- a/app/mailers/email_rejection_mailer.rb
+++ b/app/mailers/email_rejection_mailer.rb
@@ -10,7 +10,7 @@ class EmailRejectionMailer < BaseMailer
subject: "[Rejected] #{@original_message.subject}"
}
- headers['Message-ID'] = SecureRandom.hex
+ headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>"
headers['In-Reply-To'] = @original_message.message_id
headers['References'] = @original_message.message_id
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
index d58609a2de5..2f86d1be576 100644
--- a/app/mailers/emails/builds.rb
+++ b/app/mailers/emails/builds.rb
@@ -3,13 +3,27 @@ module Emails
def build_fail_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
+
+ add_project_headers
+ add_build_headers('failed')
mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
end
def build_success_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
+
+ add_project_headers
+ add_build_headers('success')
mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
end
+
+ private
+
+ def add_build_headers(status)
+ headers['X-GitLab-Build-Id'] = @build.id
+ headers['X-GitLab-Build-Ref'] = @build.ref
+ headers['X-GitLab-Build-Status'] = status.to_s
+ end
end
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index abdeefed5ef..5f9adb32e00 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -1,35 +1,51 @@
module Emails
module Issues
def new_issue_email(recipient_id, issue_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id)
+ setup_issue_mail(issue_id, recipient_id)
+
+ @label_names = label_names
+ @labels_url = namespace_project_labels_url(@project.namespace, @project)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @issue_status = status
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @issue_status = status
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
private
+ def setup_issue_mail(issue_id, recipient_id)
+ @issue = Issue.find(issue_id)
+ @project = @issue.project
+ @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
+
+ @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
+ end
+
def issue_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
@@ -37,15 +53,5 @@ module Emails
subject: subject("#{@issue.title} (##{@issue.iid})")
}
end
-
- def issue_mail_with_notification(issue_id, recipient_id)
- @issue = Issue.find(issue_id)
- @project = @issue.project
- @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
-
- yield
-
- SentNotification.record(@issue, recipient_id, reply_key)
- end
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 7923fb770d0..55bb4f65270 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -1,77 +1,63 @@
module Emails
module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_new_thread(@merge_request,
- from: sender(@merge_request.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ setup_merge_request_mail(merge_request_id, recipient_id)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ @label_names = label_names
+ @labels_url = namespace_project_labels_url(@project.namespace, @project)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @updated_by = User.find updated_by_user_id
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ setup_merge_request_mail(merge_request_id, recipient_id)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ setup_merge_request_mail(merge_request_id, recipient_id)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@mr_status = status
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ private
+
+ def setup_merge_request_mail(merge_request_id, recipient_id)
+ @merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
- @updated_by = User.find updated_by_user_id
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request)
+
+ @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
+ end
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ def merge_request_thread_options(sender_id, recipient_id)
+ {
+ from: sender(sender_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
+ }
end
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 65f37e92677..f9650df9a74 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -1,31 +1,31 @@
module Emails
module Notes
def note_commit_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @commit = @note.noteable
- @target_url = namespace_project_commit_url(*note_target_url_options)
-
- mail_answer_thread(@commit,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@commit.title} (#{@commit.short_id})"))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @commit = @note.noteable
+ @target_url = namespace_project_commit_url(*note_target_url_options)
+
+ mail_answer_thread(@commit,
+ from: sender(@note.author_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@commit.title} (#{@commit.short_id})"))
end
def note_issue_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @issue = @note.noteable
- @target_url = namespace_project_issue_url(*note_target_url_options)
- mail_answer_thread(@issue, note_thread_options(recipient_id))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @issue = @note.noteable
+ @target_url = namespace_project_issue_url(*note_target_url_options)
+ mail_answer_thread(@issue, note_thread_options(recipient_id))
end
def note_merge_request_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @merge_request = @note.noteable
- @target_url = namespace_project_merge_request_url(*note_target_url_options)
- mail_answer_thread(@merge_request, note_thread_options(recipient_id))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @merge_request = @note.noteable
+ @target_url = namespace_project_merge_request_url(*note_target_url_options)
+ mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
private
@@ -42,13 +42,11 @@ module Emails
}
end
- def note_mail_with_notification(note_id, recipient_id)
+ def setup_note_mail(note_id, recipient_id)
@note = Note.find(note_id)
@project = @note.project
- yield
-
- SentNotification.record(@note, recipient_id, reply_key)
+ @sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 3a83b083109..256cbcd73a1 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -14,7 +14,10 @@ module Emails
end
def new_ssh_key_email(key_id)
- @key = Key.find(key_id)
+ @key = Key.find_by_id(key_id)
+
+ return unless @key
+
@current_user = @user = @key.user
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index b96418679bd..377c2999d6c 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -43,7 +43,7 @@ module Emails
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
-
+
@target_url = namespace_project_url(@project.namespace, @project)
mail(to: @created_by.notification_email,
@@ -65,6 +65,10 @@ module Emails
# used in notify layout
@target_url = @message.target_url
+ @project = Project.find project_id
+
+ add_project_headers
+ headers['X-GitLab-Author'] = @message.author_username
mail(from: sender(@message.author_id, @message.send_from_committer_email?),
reply_to: @message.reply_to,
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 3bbdd9cee76..8cbc9eefc7b 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -100,17 +100,11 @@ class Notify < BaseMailer
end
def mail_thread(model, headers = {})
- if @project
- headers['X-GitLab-Project'] = @project.name
- headers['X-GitLab-Project-Id'] = @project.id
- headers['X-GitLab-Project-Path'] = @project.path_with_namespace
- end
-
+ add_project_headers
headers["X-GitLab-#{model.class.name}-ID"] = model.id
+ headers['X-GitLab-Reply-Key'] = reply_key
- if reply_key
- headers['X-GitLab-Reply-Key'] = reply_key
-
+ if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
@@ -153,4 +147,12 @@ class Notify < BaseMailer
def reply_key
@reply_key ||= SentNotification.reply_key
end
+
+ def add_project_headers
+ return unless @project
+
+ headers['X-GitLab-Project'] = @project.name
+ headers['X-GitLab-Project-Id'] = @project.id
+ headers['X-GitLab-Project-Path'] = @project.path_with_namespace
+ end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 1b3ee757040..e22da4806e6 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -5,17 +5,19 @@ class Ability
return [] unless user.is_a?(User)
return [] if user.blocked?
- case subject.class.name
- when "Project" then project_abilities(user, subject)
- when "Issue" then issue_abilities(user, subject)
- when "Note" then note_abilities(user, subject)
- when "ProjectSnippet" then project_snippet_abilities(user, subject)
- when "PersonalSnippet" then personal_snippet_abilities(user, subject)
- when "MergeRequest" then merge_request_abilities(user, subject)
- when "Group" then group_abilities(user, subject)
- when "Namespace" then namespace_abilities(user, subject)
- when "GroupMember" then group_member_abilities(user, subject)
- when "ProjectMember" then project_member_abilities(user, subject)
+ case subject
+ when CommitStatus then commit_status_abilities(user, subject)
+ when Project then project_abilities(user, subject)
+ when Issue then issue_abilities(user, subject)
+ when ExternalIssue then external_issue_abilities(user, subject)
+ when Note then note_abilities(user, subject)
+ when ProjectSnippet then project_snippet_abilities(user, subject)
+ when PersonalSnippet then personal_snippet_abilities(user, subject)
+ when MergeRequest then merge_request_abilities(user, subject)
+ when Group then group_abilities(user, subject)
+ when Namespace then namespace_abilities(user, subject)
+ when GroupMember then group_member_abilities(user, subject)
+ when ProjectMember then project_member_abilities(user, subject)
else []
end.concat(global_abilities(user))
end
@@ -25,6 +27,8 @@ class Ability
case true
when subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject)
+ when subject.is_a?(CommitStatus)
+ anonymous_commit_status_abilities(subject)
when subject.is_a?(Project) || subject.respond_to?(:project)
anonymous_project_abilities(subject)
when subject.is_a?(Group) || subject.respond_to?(:group)
@@ -45,23 +49,35 @@ class Ability
rules = [
:read_project,
:read_wiki,
- :read_issue,
:read_label,
:read_milestone,
:read_project_snippet,
:read_project_member,
:read_merge_request,
:read_note,
- :read_build,
+ :read_commit_status,
:download_code
]
+ # 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
[]
end
end
+ def anonymous_commit_status_abilities(subject)
+ rules = anonymous_project_abilities(subject.project)
+ # If subject is Ci::Build which inherits from CommitStatus filter the abilities
+ rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
+ rules
+ end
+
def anonymous_group_abilities(subject)
group = if subject.is_a?(Group)
subject
@@ -69,7 +85,7 @@ class Ability
subject.group
end
- if group && group.public_profile?
+ if group && group.projects.public_only.any?
[:read_group]
else
[]
@@ -95,24 +111,14 @@ class Ability
key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin
- team = project.team
-
- # Rules based on role in project
- if team.master?(user)
- rules.push(*project_master_rules)
-
- elsif team.developer?(user)
- rules.push(*project_dev_rules)
-
- elsif team.reporter?(user)
- rules.push(*project_report_rules)
-
- elsif team.guest?(user)
- rules.push(*project_guest_rules)
- end
+ # Push abilities on the users team role
+ rules.push(*project_team_rules(project.team, user))
- if project.public? || project.internal?
+ if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
+
+ # Allow to read builds for internal projects
+ rules << :read_build if project.public_builds?
end
if project.owner == user || user.admin?
@@ -131,10 +137,24 @@ class Ability
end
end
+ def project_team_rules(team, user)
+ # Rules based on role in project
+ if team.master?(user)
+ project_master_rules
+ elsif team.developer?(user)
+ project_dev_rules
+ elsif team.reporter?(user)
+ project_report_rules
+ elsif team.guest?(user)
+ project_guest_rules
+ end
+ end
+
def public_project_rules
@public_project_rules ||= project_guest_rules + [
:download_code,
- :fork_project
+ :fork_project,
+ :read_commit_status,
]
end
@@ -149,7 +169,6 @@ class Ability
:read_project_member,
:read_merge_request,
:read_note,
- :read_build,
:create_project,
:create_issue,
:create_note
@@ -158,24 +177,27 @@ class Ability
def project_report_rules
@project_report_rules ||= project_guest_rules + [
- :create_commit_status,
- :read_commit_statuses,
:download_code,
:fork_project,
:create_project_snippet,
:update_issue,
:admin_issue,
- :admin_label
+ :admin_label,
+ :read_commit_status,
+ :read_build,
]
end
def project_dev_rules
@project_dev_rules ||= project_report_rules + [
:admin_merge_request,
+ :update_merge_request,
+ :create_commit_status,
+ :update_commit_status,
+ :create_build,
+ :update_build,
:create_merge_request,
:create_wiki,
- :manage_builds,
- :download_build_artifacts,
:push_code
]
end
@@ -194,14 +216,15 @@ class Ability
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
- :update_merge_request,
:admin_milestone,
:admin_project_snippet,
:admin_project_member,
:admin_merge_request,
:admin_note,
:admin_wiki,
- :admin_project
+ :admin_project,
+ :admin_commit_status,
+ :admin_build
]
end
@@ -240,6 +263,10 @@ class Ability
rules += named_abilities('wiki')
end
+ unless project.builds_enabled
+ rules += named_abilities('build')
+ end
+
rules
end
@@ -296,6 +323,7 @@ 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
@@ -331,7 +359,7 @@ class Ability
]
end
- if snippet.public? || snippet.internal?
+ if snippet.public? || (snippet.internal? && !user.external?)
rules << :read_personal_snippet
end
@@ -376,6 +404,22 @@ class Ability
rules
end
+ def commit_status_abilities(user, subject)
+ rules = project_abilities(user, subject.project)
+ # If subject is Ci::Build which inherits from CommitStatus filter the abilities
+ rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
+ rules
+ end
+
+ def filter_build_abilities(rules)
+ # If we can't read build we should also not have that
+ # ability when looking at this in context of commit_status
+ %w(read create update admin).each do |rule|
+ rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build")
+ end
+ rules
+ end
+
def abilities
@abilities ||= begin
abilities = Six.new
@@ -384,6 +428,10 @@ class Ability
end
end
+ def external_issue_abilities(user, subject)
+ project_abilities(user, subject.project)
+ end
+
private
def named_abilities(name)
@@ -394,5 +442,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/abuse_report.rb b/app/models/abuse_report.rb
index 89b3116b9f2..b61f5123127 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -17,5 +17,16 @@ class AbuseReport < ActiveRecord::Base
validates :reporter, presence: true
validates :user, presence: true
validates :message, presence: true
- validates :user_id, uniqueness: true
+ validates :user_id, uniqueness: { message: 'has already been reported' }
+
+ def remove_user(deleted_by:)
+ user.block
+ DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
+ end
+
+ def notify
+ return unless self.persisted?
+
+ AbuseReportMailer.notify(self.id).deliver_later
+ end
end
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
new file mode 100644
index 00000000000..4cf8dd9a8ce
--- /dev/null
+++ b/app/models/appearance.rb
@@ -0,0 +1,9 @@
+class Appearance < ActiveRecord::Base
+ validates :title, presence: true
+ validates :description, presence: true
+ validates :logo, file_size: { maximum: 1.megabyte }
+ validates :header_logo, file_size: { maximum: 1.megabyte }
+
+ mount_uploader :logo, AttachmentUploader
+ mount_uploader :header_logo, AttachmentUploader
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index be69d317d73..269056e0e77 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -27,9 +27,23 @@
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
-# runners_registration_token :string(255)
-# require_two_factor_authentication :boolean default(TRUE)
+# runners_registration_token :string
+# require_two_factor_authentication :boolean default(FALSE)
# two_factor_grace_period :integer default(48)
+# metrics_enabled :boolean default(FALSE)
+# metrics_host :string default("localhost")
+# metrics_username :string
+# metrics_password :string
+# metrics_pool_size :integer default(16)
+# metrics_timeout :integer default(10)
+# metrics_method_call_threshold :integer default(10)
+# recaptcha_enabled :boolean default(FALSE)
+# recaptcha_site_key :string
+# recaptcha_private_key :string
+# metrics_port :integer default(8089)
+# sentry_enabled :boolean default(FALSE)
+# sentry_dsn :string
+# email_author_in_body :boolean default(FALSE)
#
class ApplicationSetting < ActiveRecord::Base
@@ -57,8 +71,8 @@ class ApplicationSetting < ActiveRecord::Base
url: true
validates :admin_notification_email,
- allow_blank: true,
- email: true
+ email: true,
+ allow_blank: true
validates :two_factor_grace_period,
numericality: { greater_than_or_equal_to: 0 }
@@ -71,6 +85,18 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :recaptcha_enabled
+ validates :sentry_dsn,
+ presence: true,
+ if: :sentry_enabled
+
+ validates :akismet_api_key,
+ presence: true,
+ if: :akismet_enabled
+
+ validates :max_attachment_size,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
@@ -126,7 +152,9 @@ class ApplicationSetting < ActiveRecord::Base
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
- two_factor_grace_period: 48
+ two_factor_grace_period: 48,
+ recaptcha_enabled: false,
+ akismet_enabled: false
)
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
new file mode 100644
index 00000000000..72e6c5fa3fd
--- /dev/null
+++ b/app/models/blob.rb
@@ -0,0 +1,37 @@
+# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
+class Blob < SimpleDelegator
+ CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
+ CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
+
+ # Wrap a Gitlab::Git::Blob object, or return nil when given nil
+ #
+ # This method prevents the decorated object from evaluating to "truthy" when
+ # given a nil value. For example:
+ #
+ # blob = Blob.new(nil)
+ # puts "truthy" if blob # => "truthy"
+ #
+ # blob = Blob.decorate(nil)
+ # puts "truthy" if blob # No output
+ def self.decorate(blob)
+ return if blob.nil?
+
+ new(blob)
+ end
+
+ def svg?
+ text? && language && language.name == 'SVG'
+ end
+
+ def to_partial_path
+ if lfs_pointer?
+ 'download'
+ elsif image? || svg?
+ 'image'
+ elsif text?
+ 'text'
+ else
+ 'download'
+ end
+ end
+end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index ad514706160..8a0a8a4c2a9 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -6,7 +6,6 @@
# message :text not null
# starts_at :datetime
# ends_at :datetime
-# alert_type :integer
# created_at :datetime
# updated_at :datetime
# color :string(255)
@@ -23,7 +22,24 @@ class BroadcastMessage < ActiveRecord::Base
validates :color, allow_blank: true, color: true
validates :font, allow_blank: true, color: true
+ default_value_for :color, '#E75E40'
+ default_value_for :font, '#FFFFFF'
+
def self.current
- where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last
+ Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do
+ where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last
+ end
+ end
+
+ def active?
+ started? && !ended?
+ end
+
+ def started?
+ Time.zone.now >= starts_at
+ end
+
+ def ended?
+ ends_at < Time.zone.now
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3e67b2771c1..7d33838044b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -29,6 +29,10 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
+# artifacts_metadata :text
+# erased_by_id :integer
+# erased_at :datetime
#
module Ci
@@ -37,6 +41,7 @@ module Ci
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
+ belongs_to :erased_by, class_name: 'User'
serialize :options
@@ -48,12 +53,15 @@ module Ci
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
mount_uploader :artifacts_file, ArtifactUploader
+ mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable
# To prevent db load megabytes of data from trace
default_scope -> { select(Ci::Build.columns_without_lazy) }
+ before_destroy { project }
+
class << self
def columns_without_lazy
(column_names - LAZY_ATTRIBUTES).map do |column_name|
@@ -97,29 +105,36 @@ module Ci
end
state_machine :status, initial: :pending do
- after_transition pending: :running do |build, transition|
+ after_transition pending: :running do |build|
build.execute_hooks
end
- after_transition any => [:success, :failed, :canceled] do |build, transition|
- return unless build.project
+ # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
+ around_transition any => [:success, :failed, :canceled] do |build, block|
+ block.call
+ build.commit.create_next_builds(build) if build.commit
+ end
+ after_transition any => [:success, :failed, :canceled] do |build|
build.update_coverage
- build.commit.create_next_builds(build)
build.execute_hooks
end
end
- def ignored?
- failed? && allow_failure?
- end
-
def retryable?
project.builds_enabled? && commands.present?
end
def retried?
- !self.commit.latest_builds_for_ref(self.ref).include?(self)
+ !self.commit.latest_statuses_for_ref(self.ref).include?(self)
+ end
+
+ def depends_on_builds
+ # Get builds of the same type
+ latest_builds = self.commit.builds.similar(self).latest
+
+ # Return builds from previous stages
+ latest_builds.where('stage_idx < ?', stage_idx)
end
def trace_html
@@ -145,10 +160,6 @@ module Ci
end
end
- def project
- commit.project
- end
-
def project_id
commit.project.id
end
@@ -169,6 +180,7 @@ module Ci
end
def update_coverage
+ return unless project
coverage_regex = project.build_coverage_regex
return unless coverage_regex
coverage = extract_coverage(trace, coverage_regex)
@@ -193,6 +205,10 @@ module Ci
end
end
+ def has_trace?
+ raw_trace.present?
+ end
+
def raw_trace
if File.file?(path_to_trace)
File.read(path_to_trace)
@@ -207,7 +223,7 @@ module Ci
def trace
trace = raw_trace
- if project && trace.present?
+ if project && trace.present? && project.runners_token.present?
trace.gsub(project.runners_token, 'xxxxxx')
else
trace
@@ -291,25 +307,6 @@ module Ci
project.valid_runners_token? token
end
- def target_url
- Gitlab::Application.routes.url_helpers.
- namespace_project_build_url(project.namespace, project, self)
- end
-
- def cancel_url
- if active?
- Gitlab::Application.routes.url_helpers.
- cancel_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
- def retry_url
- if retryable?
- Gitlab::Application.routes.url_helpers.
- retry_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
def can_be_served?(runner)
(tag_list - runner.tag_list).empty?
end
@@ -318,24 +315,55 @@ module Ci
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end
- def show_warning?
+ def stuck?
pending? && !any_runners_online?
end
- def download_url
- if artifacts_file.exists?
- Gitlab::Application.routes.url_helpers.
- download_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
def execute_hooks
+ return unless project
build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
end
+ def artifacts?
+ artifacts_file.exists?
+ end
+
+ def artifacts_metadata?
+ artifacts? && artifacts_metadata.exists?
+ end
+
+ def artifacts_metadata_entry(path, **options)
+ Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
+ end
+
+ def erase(opts = {})
+ return false unless erasable?
+
+ remove_artifacts_file!
+ remove_artifacts_metadata!
+ erase_trace!
+ update_erased!(opts[:erased_by])
+ end
+ def erasable?
+ complete? && (artifacts? || has_trace?)
+ end
+
+ def erased?
+ !self.erased_at.nil?
+ end
+
+ private
+
+ def erase_trace!
+ self.trace = nil
+ end
+
+ def update_erased!(user = nil)
+ self.update(erased_by: user, erased_at: Time.now)
+ end
private
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index d2a29236942..f4cf7034b14 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -25,8 +25,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
- scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
-
validates_presence_of :sha
validate :valid_commit_sha
@@ -42,16 +40,6 @@ module Ci
project.id
end
- def last_build
- builds.order(:id).last
- end
-
- def retry
- latest_builds.each do |build|
- Ci::Build.retry(build)
- end
- end
-
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -121,12 +109,14 @@ module Ci
@latest_statuses ||= statuses.latest.to_a
end
- def latest_builds
- @latest_builds ||= builds.latest.to_a
+ def latest_statuses_for_ref(ref)
+ latest_statuses.select { |status| status.ref == ref }
end
- def latest_builds_for_ref(ref)
- latest_builds.select { |build| build.ref == ref }
+ def matrix_builds(build = nil)
+ matrix_builds = builds.latest.ordered
+ matrix_builds = matrix_builds.similar(build) if build
+ matrix_builds.to_a
end
def retried
@@ -170,7 +160,7 @@ module Ci
end
def duration
- duration_array = latest_statuses.map(&:duration).compact
+ duration_array = statuses.map(&:duration).compact
duration_array.reduce(:+).to_i
end
@@ -183,16 +173,12 @@ module Ci
end
def coverage
- coverage_array = latest_builds.map(&:coverage).compact
+ coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
- def matrix_for_ref?(ref)
- latest_builds_for_ref(ref).size > 1
- end
-
def config_processor
return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
@@ -205,7 +191,11 @@ module Ci
end
def ci_yaml_file
- @ci_yaml_file ||= project.repository.blob_at(sha, '.gitlab-ci.yml').data
+ @ci_yaml_file ||= begin
+ blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
+ blob.load_all_data!(project.repository)
+ blob.data
+ end
rescue
nil
end
@@ -214,10 +204,6 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
- def update_committed!
- update!(committed_at: DateTime.now)
- end
-
private
def save_yaml_error(error)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 38b20cd7faa..90349a07594 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -22,7 +22,8 @@ module Ci
extend Ci::Model
LAST_CONTACT_TIME = 5.minutes.ago
-
+ AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
+
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
@@ -38,11 +39,30 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
+ scope :owned_or_shared, ->(project_id) do
+ joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
+ .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ end
+
acts_as_taggable
+ # Searches for runners matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # This method performs a *partial* match on tokens, thus a query for "a"
+ # will match any runner where the token contains the letter "a". As a result
+ # you should *not* use this method for non-admin purposes as otherwise users
+ # might be able to query a list of all runners.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def self.search(query)
- where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
- query: "%#{query.try(:downcase)}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end
def set_default_values
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 93d9be144e8..7b16f207a26 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -2,11 +2,12 @@
#
# Table name: ci_runner_projects
#
-# id :integer not null, primary key
-# runner_id :integer not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
module Ci
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 23516709a41..2b9a457c8ab 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -2,12 +2,13 @@
#
# Table name: ci_triggers
#
-# id :integer not null, primary key
-# token :string(255)
-# project_id :integer not null
-# deleted_at :datetime
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
module Ci
@@ -32,6 +33,10 @@ module Ci
trigger_requests.last
end
+ def last_used
+ last_trigger_request.try(:created_at)
+ end
+
def short_token
token[0...10]
end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 56759d3e50f..e786bd7dd93 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -3,12 +3,13 @@
# Table name: ci_variables
#
# id :integer not null, primary key
-# project_id :integer not null
+# project_id :integer
# key :string(255)
# value :text
# encrypted_value :text
# encrypted_value_salt :string(255)
# encrypted_value_iv :string(255)
+# gl_project_id :integer
#
module Ci
@@ -17,8 +18,12 @@ module Ci
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- validates_presence_of :key
validates_uniqueness_of :key, scope: :gl_project_id
+ validates :key,
+ presence: true,
+ length: { within: 0..255 },
+ format: { with: /\A[a-zA-Z0-9_]+\z/,
+ message: "can contain only letters, digits and '_'." }
attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 0ba7b584d91..ce0b85d50cf 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -12,12 +12,7 @@ class Commit
attr_accessor :project
- # Safe amount of changes (files and lines) in one commit to render
- # Used to prevent 500 error on huge commits by suppressing diff
- #
- # User can force display of diff above this size
- DIFF_SAFE_FILES = 100 unless defined?(DIFF_SAFE_FILES)
- DIFF_SAFE_LINES = 5000 unless defined?(DIFF_SAFE_LINES)
+ DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
# Commits above this size will not be rendered in HTML
DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES)
@@ -36,13 +31,20 @@ class Commit
# Calculate number of lines to render for diffs
def diff_line_count(diffs)
- diffs.reduce(0) { |sum, d| sum + d.diff.lines.count }
+ diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) }
end
# Truncate sha to 8 characters
def truncate_sha(sha)
sha[0..7]
end
+
+ def max_diff_options
+ {
+ max_files: DIFF_HARD_LIMIT_FILES,
+ max_lines: DIFF_HARD_LIMIT_LINES,
+ }
+ end
end
attr_accessor :raw
@@ -68,18 +70,18 @@ class Commit
# Pattern used to extract commit references from text
#
- # The SHA can be between 6 and 40 hex characters.
+ # The SHA can be between 7 and 40 hex characters.
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
(?:#{Project.reference_pattern}#{reference_prefix})?
- (?<commit>\h{6,40})
+ (?<commit>\h{7,40})
}x
end
def self.link_reference_pattern
- super("commit", /(?<commit>\h{6,40})/)
+ super("commit", /(?<commit>\h{7,40})/)
end
def to_reference(from_project = nil)
@@ -215,6 +217,44 @@ class Commit
ci_commit.try(:status) || :not_found
end
+ def revert_branch_name
+ "revert-#{short_id}"
+ end
+
+ def revert_description
+ if merged_merge_request
+ "This reverts merge request #{merged_merge_request.to_reference}"
+ else
+ "This reverts commit #{sha}"
+ end
+ end
+
+ def revert_message
+ %Q{Revert "#{title}"\n\n#{revert_description}}
+ end
+
+ def reverts_commit?(commit)
+ description? && description.include?(commit.revert_description)
+ end
+
+ def merge_commit?
+ parents.size > 1
+ end
+
+ def merged_merge_request
+ return @merged_merge_request if defined?(@merged_merge_request)
+
+ @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit?
+ end
+
+ def has_been_reverted?(current_user = nil, noteable = self)
+ Gitlab::ReferenceExtractor.lazily do
+ noteable.notes.system.flat_map do |note|
+ note.all_references(current_user).commits
+ end
+ end.any? { |commit_ref| commit_ref.reverts_commit?(self) }
+ end
+
private
def repo_changes
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 14e7971fa06..289dbc57287 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -32,8 +32,8 @@ class CommitRange
PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/
# In text references, the beginning and ending refs can only be SHAs
- # between 6 and 40 hex characters.
- STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
+ # between 7 and 40 hex characters.
+ STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/
def self.reference_prefix
'@'
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 21c5c87bc3d..3377a85a55a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,30 +1,35 @@
# == Schema Information
#
-# project_id integer
-# status string
-# finished_at datetime
-# trace text
-# created_at datetime
-# updated_at datetime
-# started_at datetime
-# runner_id integer
-# coverage float
-# commit_id integer
-# commands text
-# job_id integer
-# name string
-# deploy boolean default: false
-# options text
-# allow_failure boolean default: false, null: false
-# stage string
-# trigger_request_id integer
-# stage_idx integer
-# tag boolean
-# ref string
-# user_id integer
-# type string
-# target_url string
-# description string
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+# gl_project_id :integer
#
class CommitStatus < ActiveRecord::Base
@@ -51,6 +56,8 @@ class CommitStatus < ActiveRecord::Base
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) }
+ AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled']
+
state_machine :status, initial: :pending do
event :run do
transition pending: :running
@@ -68,16 +75,16 @@ class CommitStatus < ActiveRecord::Base
transition [:pending, :running] => :canceled
end
- after_transition pending: :running do |build, transition|
- build.update_attributes started_at: Time.now
+ after_transition pending: :running do |commit_status|
+ commit_status.update_attributes started_at: Time.now
end
- after_transition any => [:success, :failed, :canceled] do |build, transition|
- build.update_attributes finished_at: Time.now
+ after_transition any => [:success, :failed, :canceled] do |commit_status|
+ commit_status.update_attributes finished_at: Time.now
end
- after_transition [:pending, :running] => :success do |build, transition|
- MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.project, nil).trigger(build)
+ after_transition [:pending, :running] => :success do |commit_status|
+ MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end
state :pending, value: 'pending'
@@ -106,6 +113,10 @@ class CommitStatus < ActiveRecord::Base
canceled? || success? || failed?
end
+ def ignored?
+ allow_failure? && (failed? || canceled?)
+ end
+
def duration
if started_at && finished_at
finished_at - started_at
@@ -114,19 +125,7 @@ class CommitStatus < ActiveRecord::Base
end
end
- def cancel_url
- nil
- end
-
- def retry_url
- nil
- end
-
- def show_warning?
+ def stuck?
false
end
-
- def download_url
- nil
- end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 18a00f95b48..86ab84615ba 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -8,6 +8,7 @@ module Issuable
extend ActiveSupport::Concern
include Participable
include Mentionable
+ include Subscribable
include StripAttribute
included do
@@ -18,7 +19,6 @@ 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 :subscriptions, dependent: :destroy, as: :subscribable
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -29,15 +29,19 @@ module Issuable
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
+ scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :opened, -> { with_state(:opened, :reopened) }
scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
+ scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
+ scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
+ scope :non_archived, -> { join_project.merge(Project.non_archived) }
delegate :name,
:email,
@@ -57,22 +61,64 @@ module Issuable
end
module ClassMethods
+ # Searches for records with a matching title.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(title) like :query", query: "%#{query.downcase}%")
+ where(arel_table[:title].matches("%#{query}%"))
end
+ # Searches for records with a matching title or description.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def full_search(query)
- where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
def sort(method)
case method.to_s
when 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
+ when 'downvotes_desc' then order_downvotes_desc
+ when 'upvotes_desc' then order_upvotes_desc
else
order_by(method)
end
end
+
+ def order_downvotes_desc
+ order_votes_desc('thumbsdown')
+ end
+
+ def order_upvotes_desc
+ order_votes_desc('thumbsup')
+ end
+
+ def order_votes_desc(award_emoji_name)
+ issuable_table = self.arel_table
+ note_table = Note.arel_table
+
+ join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on(
+ note_table[:noteable_id].eq(issuable_table[:id]).and(
+ note_table[:noteable_type].eq(self.name).and(
+ note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name))
+ )
+ )
+ ).join_sources
+
+ joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
+ end
end
def today?
@@ -103,34 +149,22 @@ module Issuable
notes.awards.where(note: "thumbsup").count
end
- def subscribed?(user)
- subscription = subscriptions.find_by_user_id(user.id)
-
- if subscription
- return subscription.subscribed
- end
-
+ def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
- def toggle_subscription(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: !subscribed?(user))
- end
-
def to_hook_data(user)
- {
+ hook_data = {
object_kind: self.class.name.underscore,
user: user.hook_attrs,
- repository: {
- name: project.name,
- url: project.url_to_repo,
- description: project.description,
- homepage: project.web_url
- },
- object_attributes: hook_attrs
+ project: project.hook_attrs,
+ object_attributes: hook_attrs,
+ # DEPRECATED
+ repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
+ hook_data.merge!(assignee: assignee.hook_attrs) if assignee
+
+ hook_data
end
def label_names
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 6316ee208b5..98f71ae8cb0 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -51,8 +51,11 @@ module Mentionable
else
self.class.mentionable_attrs.each do |attr, options|
text = send(attr)
- options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted?
- ext.analyze(text, options)
+
+ context = options.dup
+ context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted?
+
+ ext.analyze(text, context)
end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
new file mode 100644
index 00000000000..5b8e3f654ea
--- /dev/null
+++ b/app/models/concerns/milestoneish.rb
@@ -0,0 +1,29 @@
+module Milestoneish
+ def closed_items_count(user = nil)
+ issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
+ end
+
+ def total_items_count(user = nil)
+ issues_visible_to_user(user).size + merge_requests.size
+ end
+
+ def complete?(user = nil)
+ total_items_count(user) == closed_items_count(user)
+ end
+
+ def percent_complete(user = nil)
+ ((closed_items_count(user) * 100) / total_items_count(user)).abs
+ rescue ZeroDivisionError
+ 0
+ end
+
+ def remaining_days
+ return 0 if !due_date || expired?
+
+ (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/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 7391a77383c..8b47b9e0abd 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -11,6 +11,7 @@ module Sortable
default_scope { order_id_desc }
scope :order_id_desc, -> { reorder(id: :desc) }
+ scope :order_id_asc, -> { reorder(id: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) }
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
@@ -28,6 +29,8 @@ module Sortable
when 'updated_desc' then order_updated_desc
when 'created_asc' then order_created_asc
when 'created_desc' then order_created_desc
+ when 'id_desc' then order_id_desc
+ when 'id_asc' then order_id_asc
else
all
end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
new file mode 100644
index 00000000000..d5a881b2445
--- /dev/null
+++ b/app/models/concerns/subscribable.rb
@@ -0,0 +1,44 @@
+# == Subscribable concern
+#
+# Users can subscribe to these models.
+#
+# Used by Issue, MergeRequest, Label
+#
+
+module Subscribable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :subscriptions, dependent: :destroy, as: :subscribable
+ end
+
+ def subscribed?(user)
+ if subscription = subscriptions.find_by_user_id(user.id)
+ subscription.subscribed
+ else
+ subscribed_without_subscriptions?(user)
+ end
+ end
+
+ # Override this method to define custom logic to consider a subscribable as
+ # subscribed without an explicit subscription record.
+ def subscribed_without_subscriptions?(user)
+ false
+ end
+
+ def subscribers
+ subscriptions.where(subscribed: true).map(&:user)
+ end
+
+ def toggle_subscription(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: !subscribed?(user))
+ end
+
+ def unsubscribe(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: false)
+ end
+end
diff --git a/app/models/diff_line.rb b/app/models/diff_line.rb
deleted file mode 100644
index ad37945874a..00000000000
--- a/app/models/diff_line.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class DiffLine
- attr_accessor :type, :content, :num, :code
-end
diff --git a/app/models/email.rb b/app/models/email.rb
index 935705e2ed4..b323d1edd10 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -15,7 +15,7 @@ class Email < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
- validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
+ validates :email, presence: true, uniqueness: true, email: true
validate :unique_email, if: ->(email) { email.email_changed? }
before_validation :cleanup_email
diff --git a/app/models/event.rb b/app/models/event.rb
index 01d008035a5..a5cfeaf388e 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -47,7 +47,11 @@ class Event < ActiveRecord::Base
# Scopes
scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) }
- scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
+
+ scope :in_projects, ->(projects) do
+ where(project_id: projects.map(&:id)).recent
+ end
+
scope :with_associations, -> { includes(project: :namespace) }
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
@@ -64,26 +68,22 @@ class Event < ActiveRecord::Base
[Event::CREATED, Event::CLOSED, Event::MERGED])
end
- def latest_update_time
- row = select(:updated_at, :project_id).reorder(id: :desc).take
-
- row ? row.updated_at : nil
- end
-
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
end
- def proper?
+ def proper?(user = nil)
if push?
true
elsif membership_changed?
true
elsif created_project?
true
+ elsif issue?
+ Ability.abilities.allowed?(user, :read_issue, issue)
else
- ((issue? || merge_request? || note?) && target) || milestone?
+ ((merge_request? || note?) && target) || milestone?
end
end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 49f6c95e045..2ca79df0a29 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>([A-Z\-]+-)\d+)}
+ %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil)
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index 12c934e2494..97f4f03a9a5 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -29,6 +29,7 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
#
class GenericCommitStatus < CommitStatus
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index 0171f7d54b7..ddd4bad5c21 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -2,16 +2,19 @@ class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
+ delegate :color, :description, to: :@first_label
+
def self.build_collection(labels)
labels = labels.group_by(&:title)
- labels.map do |title, label|
- new(title, label)
+ labels.map do |title, labels|
+ new(title, labels)
end
end
def initialize(title, labels)
@title = title
@labels = labels
+ @first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index af1d7562ebe..97bd79af083 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -1,4 +1,6 @@
class GlobalMilestone
+ include Milestoneish
+
attr_accessor :title, :milestones
alias_attribute :name, :title
@@ -28,33 +30,7 @@ class GlobalMilestone
end
def projects
- milestones.map { |milestone| milestone.project }
- end
-
- def issue_count
- milestones.map { |milestone| milestone.issues.count }.sum
- end
-
- def merge_requests_count
- milestones.map { |milestone| milestone.merge_requests.count }.sum
- end
-
- def open_items_count
- milestones.map { |milestone| milestone.open_items_count }.sum
- end
-
- def closed_items_count
- milestones.map { |milestone| milestone.closed_items_count }.sum
- end
-
- def total_items_count
- milestones.map { |milestone| milestone.total_items_count }.sum
- end
-
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
- rescue ZeroDivisionError
- 0
+ @projects ||= Project.for_milestones(milestones.map(&:id))
end
def state
@@ -76,35 +52,20 @@ class GlobalMilestone
end
def issues
- @issues ||= milestones.map(&:issues).flatten.group_by(&:state)
+ @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
end
def merge_requests
- @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
+ @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
end
def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq
end
- def opened_issues
- issues.values_at("opened", "reopened").compact.flatten
- end
-
- def closed_issues
- issues['closed']
- end
-
- def opened_merge_requests
- merge_requests.values_at("opened", "reopened").compact.flatten
- end
-
- def closed_merge_requests
- merge_requests.values_at("closed", "merged", "locked").compact.flatten
- end
-
- def complete?
- total_items_count == closed_items_count
+ def labels
+ @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
+ .sort_by!(&:title)
end
def due_date
@@ -121,9 +82,9 @@ class GlobalMilestone
def expires_at
if due_date
if due_date.past?
- "expired at #{due_date.stamp("Aug 21, 2011")}"
+ "expired on #{due_date.to_s(:medium)}"
else
- "expires at #{due_date.stamp("Aug 21, 2011")}"
+ "expires on #{due_date.to_s(:medium)}"
end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 1b5b875a19e..9919ca112dc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -20,10 +19,12 @@ require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
include Referable
-
+
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
+ has_many :project_group_links, dependent: :destroy
+ has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
@@ -34,8 +35,18 @@ class Group < Namespace
after_destroy :post_destroy_hook
class << self
+ # Searches for groups matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
+ table = Namespace.arel_table
+ pattern = "%#{query}%"
+
+ where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end
def sort(method)
@@ -50,10 +61,6 @@ class Group < Namespace
User.reference_pattern
end
- def public_and_given_groups(ids)
- where('public IS TRUE OR namespaces.id IN (?)', ids)
- end
-
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
@@ -125,10 +132,6 @@ class Group < Namespace
end
end
- def public_profile?
- self.public || projects.public_only.any?
- end
-
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 22638057773..fe923fafbe0 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -3,11 +3,11 @@
# Table name: web_hooks
#
# id :integer not null, primary key
-# url :string(255)
+# url :string(2000)
# project_id :integer
# created_at :datetime
# updated_at :datetime
-# type :string(255) default("ProjectHook")
+# type :string default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class ProjectHook < WebHook
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 09bb3ee52a2..80962264ba2 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -3,11 +3,11 @@
# Table name: web_hooks
#
# id :integer not null, primary key
-# url :string(255)
+# url :string(2000)
# project_id :integer
# created_at :datetime
# updated_at :datetime
-# type :string(255) default("ProjectHook")
+# type :string default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class ServiceHook < WebHook
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 2f63c59b07e..c147d8762a9 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -3,11 +3,11 @@
# Table name: web_hooks
#
# id :integer not null, primary key
-# url :string(255)
+# url :string(2000)
# project_id :integer
# created_at :datetime
# updated_at :datetime
-# type :string(255) default("ProjectHook")
+# type :string default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class SystemHook < WebHook
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 40eb0e20b4b..7a13c3f0a39 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,11 +3,11 @@
# Table name: web_hooks
#
# id :integer not null, primary key
-# url :string(255)
+# url :string(2000)
# project_id :integer
# created_at :datetime
# updated_at :datetime
-# type :string(255) default("ProjectHook")
+# type :string default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class WebHook < ActiveRecord::Base
@@ -47,8 +48,8 @@ class WebHook < ActiveRecord::Base
else
post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = {
- username: URI.decode(parsed_url.user),
- password: URI.decode(parsed_url.password),
+ username: CGI.unescape(parsed_url.user),
+ password: CGI.unescape(parsed_url.password),
}
response = WebHook.post(post_url,
body: data.to_json,
@@ -60,7 +61,7 @@ class WebHook < ActiveRecord::Base
basic_auth: auth)
end
- [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)]
+ [(response.code >= 200 && response.code < 300), ActionView::Base.full_sanitizer.sanitize(response.to_s)]
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
logger.error("WebHook Error => #{e}")
[false, e.to_s]
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 8bcdc194953..e1915b079d4 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -18,4 +18,8 @@ class Identity < ActiveRecord::Base
validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider }
+
+ def ldap?
+ provider.starts_with?('ldap')
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 80ecd15077f..053387cffd7 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -33,9 +33,12 @@ class Issue < ActiveRecord::Base
belongs_to :project
validates :project, presence: true
- scope :of_group, ->(group) { where(project_id: group.project_ids) }
+ scope :of_group,
+ ->(group) { where(project_id: group.projects.select(:id).reorder(nil)) }
+
scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
+ scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
state_machine :state, initial: :opened do
event :close do
@@ -55,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
@@ -83,12 +93,22 @@ class Issue < ActiveRecord::Base
reference
end
- def referenced_merge_requests
- Gitlab::ReferenceExtractor.lazily do
- [self, *notes].flat_map do |note|
- note.all_references.merge_requests
- end
- end.sort_by(&:iid)
+ def referenced_merge_requests(current_user = nil)
+ @referenced_merge_requests ||= {}
+ @referenced_merge_requests[current_user] ||= begin
+ Gitlab::ReferenceExtractor.lazily do
+ [self, *notes].flat_map do |note|
+ note.all_references(current_user).merge_requests
+ end
+ end.sort_by(&:iid).uniq
+ end
+ end
+
+ def related_branches
+ return [] if self.project.empty_repo?
+ self.project.repository.branch_names.select do |branch|
+ branch =~ /\A#{iid}-(?!\d+-stable)/i
+ end
end
# Reset issue events cache
@@ -117,4 +137,15 @@ class Issue < ActiveRecord::Base
note.all_references(current_user).merge_requests
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
end
+
+ def to_branch_name
+ "#{iid}-#{title.parameterize}"
+ end
+
+ def can_be_worked_on?(current_user)
+ !self.closed? &&
+ !self.project.forked? &&
+ self.related_branches.empty? &&
+ self.closed_by_merge_requests(current_user).empty?
+ end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 406a1257b5d..0282ad18139 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -16,6 +16,7 @@
require 'digest/md5'
class Key < ActiveRecord::Base
+ include AfterCommitQueue
include Sortable
belongs_to :user
@@ -62,7 +63,7 @@ class Key < ActiveRecord::Base
end
def notify_user
- NotificationService.new.new_key(self)
+ run_after_commit { NotificationService.new.new_key(self) }
end
def post_create_hook
diff --git a/app/models/label.rb b/app/models/label.rb
index 220da10a6ab..f7ffc0b7f36 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -2,17 +2,20 @@
#
# Table name: labels
#
-# id :integer not null, primary key
-# title :string(255)
-# color :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# template :boolean default(FALSE)
+# id :integer not null, primary key
+# title :string(255)
+# color :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# template :boolean default(FALSE)
+# description :string(255)
#
class Label < ActiveRecord::Base
include Referable
+ include Subscribable
+
# Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned.
LabelStruct = Struct.new(:title, :name)
@@ -26,6 +29,7 @@ class Label < ActiveRecord::Base
belongs_to :project
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
+ has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
@@ -46,10 +50,15 @@ class Label < ActiveRecord::Base
'~'
end
+ ##
# Pattern used to extract label references from text
+ #
+ # This pattern supports cross-project references.
+ #
def self.reference_pattern
%r{
- #{reference_prefix}
+ (#{Project.reference_pattern})?
+ #{Regexp.escape(reference_prefix)}
(?:
(?<label_id>\d+) | # Integer-based label ID, or
(?<label_name>
@@ -60,24 +69,31 @@ class Label < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ nil
+ end
+
+ ##
# Returns the String necessary to reference this Label in Markdown
#
# format - Symbol format to use (default: :id, optional: :name)
#
- # Note that its argument differs from other objects implementing Referable. If
- # a non-Symbol argument is given (such as a Project), it will default to :id.
- #
# Examples:
#
- # Label.first.to_reference # => "~1"
- # Label.first.to_reference(:name) # => "~\"bug\""
+ # Label.first.to_reference # => "~1"
+ # Label.first.to_reference(format: :name) # => "~\"bug\""
+ # Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
- def to_reference(format = :id)
- if format == :name && !name.include?('"')
- %(#{self.class.reference_prefix}"#{name}")
+ #
+ def to_reference(from_project = nil, format: :id)
+ format_reference = label_format_reference(format)
+ reference = "#{self.class.reference_prefix}#{format_reference}"
+
+ if cross_project_reference?(from_project)
+ project.to_reference + reference
else
- "#{self.class.reference_prefix}#{id}"
+ reference
end
end
@@ -85,7 +101,27 @@ class Label < ActiveRecord::Base
issues.opened.count
end
+ def closed_issues_count
+ issues.closed.count
+ end
+
+ def open_merge_requests_count
+ merge_requests.opened.count
+ end
+
def template?
template
end
+
+ private
+
+ def label_format_reference(format = :id)
+ raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
+
+ if format == :name && !name.include?('"')
+ %("#{name}")
+ else
+ id
+ end
+ end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 28aee2e3799..ca08007b7eb 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -39,7 +39,6 @@ class Member < ActiveRecord::Base
if: :invite?
},
email: {
- strict_mode: true,
allow_nil: true
},
uniqueness: {
@@ -91,7 +90,7 @@ class Member < ActiveRecord::Base
member.invite_email = user
end
- if can_update_member?(current_user, member)
+ if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
@@ -107,6 +106,11 @@ class Member < ActiveRecord::Base
current_user.can?(:update_group_member, member) ||
current_user.can?(:update_project_member, member)
end
+
+ def project_creator?(member, access_level)
+ member.new_record? && member.owner? &&
+ access_level.to_i == ProjectMember::MASTER
+ end
end
def invite?
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 1b0c76917aa..560d1690e14 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -84,7 +84,7 @@ class ProjectMember < Member
def truncate_teams(project_ids)
ProjectMember.transaction do
members = ProjectMember.where(source_id: project_ids)
-
+
members.each do |member|
member.destroy
end
@@ -133,13 +133,13 @@ class ProjectMember < Member
event_service.join_project(self.project, self.user)
notification_service.new_project_member(self)
end
-
+
super
end
def post_update_hook
if access_level_changed?
- notification_service.update_project_member(self)
+ notification_service.update_project_member(self)
end
super
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index ac25d38eb63..30a7bd47be7 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -2,28 +2,29 @@
#
# Table name: merge_requests
#
-# id :integer not null, primary key
-# target_branch :string(255) not null
-# source_branch :string(255) not null
-# source_project_id :integer not null
-# author_id :integer
-# assignee_id :integer
-# title :string(255)
-# created_at :datetime
-# updated_at :datetime
-# milestone_id :integer
-# state :string(255)
-# merge_status :string(255)
-# target_project_id :integer not null
-# iid :integer
-# description :text
-# position :integer default(0)
-# locked_at :datetime
-# updated_by_id :integer
-# merge_error :string(255)
-# merge_params :text (serialized to hash)
-# merge_when_build_succeeds :boolean default(false), not null
-# merge_user_id :integer
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# source_project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# created_at :datetime
+# updated_at :datetime
+# milestone_id :integer
+# state :string(255)
+# merge_status :string(255)
+# target_project_id :integer not null
+# iid :integer
+# description :text
+# position :integer default(0)
+# locked_at :datetime
+# updated_by_id :integer
+# merge_error :string(255)
+# merge_params :text
+# merge_when_build_succeeds :boolean default(FALSE), not null
+# merge_user_id :integer
+# merge_commit_sha :string
#
require Rails.root.join("app/models/commit")
@@ -47,7 +48,7 @@ class MergeRequest < ActiveRecord::Base
after_create :create_merge_request_diff
after_update :update_merge_request_diff
- delegate :commits, :diffs, :diffs_no_whitespace, to: :merge_request_diff, prefix: nil
+ delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -55,8 +56,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
- attr_accessor :can_be_created, :compare_failed,
- :compare_commits, :compare_diffs
+ attr_accessor :can_be_created, :compare_commits, :compare
state_machine :state, initial: :opened do
event :close do
@@ -131,14 +131,12 @@ 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.project_ids) }
+ 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) }
- scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :merged, -> { with_state(:merged) }
- scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :join_project, -> { joins(:target_project) }
@@ -162,6 +160,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/)
end
+ # Returns all the merge requests from an ActiveRecord:Relation.
+ #
+ # This method uses a UNION as it usually operates on the result of
+ # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
+ # using multiple sub-queries especially when combined with an OR statement.
+ # UNIONs on the other hand perform much better in these cases.
+ #
+ # relation - An ActiveRecord::Relation that returns a list of Projects.
+ #
+ # Returns an ActiveRecord::Relation.
+ def self.in_projects(relation)
+ source = where(source_project_id: relation).select(:id)
+ target = where(target_project_id: relation).select(:id)
+ union = Gitlab::SQL::Union.new([source, target])
+
+ where("merge_requests.id IN (#{union.to_sql})")
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -180,6 +196,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
+ def diff_size
+ merge_request_diff.size
+ end
+
+ def diff_base_commit
+ if merge_request_diff
+ merge_request_diff.base_commit
+ elsif source_sha
+ self.target_project.merge_base_commit(self.source_sha, self.target_branch)
+ end
+ end
+
def last_commit_short_sha
last_commit.short_id
end
@@ -229,8 +257,10 @@ class MergeRequest < ActiveRecord::Base
end
def check_if_can_be_merged
+ return unless unchecked?
+
can_be_merged =
- project.repository.can_be_merged?(source_sha, target_branch)
+ !broken? && project.repository.can_be_merged?(source_sha, target_branch)
if can_be_merged
mark_as_mergeable
@@ -248,11 +278,15 @@ class MergeRequest < ActiveRecord::Base
end
def work_in_progress?
- !!(title =~ /\A\[?WIP\]?:? /i)
+ !!(title =~ /\A\[?WIP(\]|:| )/i)
end
def mergeable?
- open? && !work_in_progress? && can_be_merged?
+ return false unless open? && !work_in_progress? && !broken?
+
+ check_if_can_be_merged
+
+ can_be_merged?
end
def gitlab_merge_status
@@ -270,7 +304,8 @@ class MergeRequest < ActiveRecord::Base
def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
- Ability.abilities.allowed?(current_user, :push_code, source_project)
+ Ability.abilities.allowed?(current_user, :push_code, source_project) &&
+ last_commit == source_project.commit(source_branch)
end
def mr_and_commit_notes
@@ -332,10 +367,10 @@ class MergeRequest < ActiveRecord::Base
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- issues = commits.flat_map { |c| c.closes_issues(current_user) }
- issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user).
- closed_by_message(description))
- issues.uniq(&:id)
+ messages = commits.map(&:safe_message) << description
+
+ Gitlab::ClosingIssueExtractor.new(project, current_user).
+ closed_by_message(messages.join("\n"))
else
[]
end
@@ -452,6 +487,10 @@ class MergeRequest < ActiveRecord::Base
!source_branch_exists? || !target_branch_exists?
end
+ def broken?
+ self.commits.blank? || branch_missing? || cannot_be_merged?
+ end
+
def can_be_merged_by?(user)
::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
end
@@ -466,13 +505,26 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def state_icon_name
+ if merged?
+ "check"
+ elsif closed?
+ "times"
+ else
+ "circle-o"
+ end
+ end
+
def target_sha
- @target_sha ||= target_project.
- repository.commit(target_branch).sha
+ @target_sha ||= target_project.repository.commit(target_branch).sha
end
def source_sha
- commits.first.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
@@ -504,11 +556,44 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def diverged_commits_count
+ cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
+
+ if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha
+ cache = {
+ source_sha: source_sha,
+ target_sha: target_sha,
+ diverged_commits_count: compute_diverged_commits_count
+ }
+ Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
+ end
+
+ cache[:diverged_commits_count]
+ end
+
+ def compute_diverged_commits_count
+ Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
+ end
+
+ def diverged_from_target_branch?
+ diverged_commits_count > 0
+ end
+
def ci_commit
@ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
end
- def broken?
- self.commits.blank? || branch_missing? || cannot_be_merged?
+ def diff_refs
+ return nil unless diff_base_commit
+
+ [diff_base_commit, last_commit]
+ end
+
+ def merge_commit
+ @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
+ end
+
+ def can_be_reverted?(current_user = nil)
+ merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c499a4b5b4c..33884118595 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -17,9 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable
# Prevent store of diff if commits amount more then 500
- COMMITS_SAFE_SIZE = 500
-
- attr_reader :commits, :diffs, :diffs_no_whitespace
+ COMMITS_SAFE_SIZE = 100
belongs_to :merge_request
@@ -27,6 +25,9 @@ class MergeRequestDiff < ActiveRecord::Base
state_machine :state, initial: :empty do
state :collected
+ state :overflow
+ # Deprecated states: these are no longer used but these values may still occur
+ # in the database.
state :timeout
state :overflow_commits_safe_size
state :overflow_diff_files_limit
@@ -43,22 +44,23 @@ class MergeRequestDiff < ActiveRecord::Base
reload_diffs
end
- def diffs
- @diffs ||= (load_diffs(st_diffs) || [])
+ def size
+ real_size.presence || diffs.size
end
- def diffs_no_whitespace
- # Get latest sha of branch from source project
- source_sha = merge_request.source_project.commit(source_branch).sha
-
- compare_result = Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- merge_request.target_project.repository.raw_repository,
- merge_request.target_branch,
- source_sha,
- ), { ignore_whitespace_change: true }
- )
- @diffs_no_whitespace ||= load_diffs(dump_commits(compare_result.diffs))
+ def diffs(options={})
+ if options[:ignore_whitespace_change]
+ @diffs_no_whitespace ||= begin
+ compare = Gitlab::Git::Compare.new(
+ self.repository.raw_repository,
+ self.target_branch,
+ self.source_sha,
+ )
+ compare.diffs(options)
+ end
+ else
+ @diffs ||= load_diffs(st_diffs, options)
+ end
end
def commits
@@ -73,12 +75,16 @@ class MergeRequestDiff < ActiveRecord::Base
commits.last
end
+ def base_commit
+ return nil unless self.base_commit_sha
+
+ merge_request.target_project.commit(self.base_commit_sha)
+ end
+
def last_commit_short_sha
@last_commit_short_sha ||= last_commit.short_id
end
- private
-
def dump_commits(commits)
commits.map(&:to_hash)
end
@@ -93,16 +99,18 @@ class MergeRequestDiff < ActiveRecord::Base
end
end
- def load_diffs(raw)
- if raw.respond_to?(:map)
- raw.map { |hash| Gitlab::Git::Diff.new(hash) }
+ def load_diffs(raw, options)
+ if raw.respond_to?(:each)
+ Gitlab::Git::DiffCollection.new(raw, options)
+ else
+ Gitlab::Git::DiffCollection.new([])
end
end
# Collect array of Git::Commit objects
# between target and source branches
def unmerged_commits
- commits = compare_result.commits
+ commits = compare.commits
if commits.present?
commits = Commit.decorate(commits, merge_request.source_project).
@@ -132,64 +140,55 @@ class MergeRequestDiff < ActiveRecord::Base
if commits.size.zero?
self.state = :empty
- elsif commits.size > COMMITS_SAFE_SIZE
- self.state = :overflow_commits_safe_size
else
- new_diffs = unmerged_diffs
- end
+ diff_collection = unmerged_diffs
- if new_diffs.any?
- if new_diffs.size > Commit::DIFF_HARD_LIMIT_FILES
- self.state = :overflow_diff_files_limit
- new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES)
+ if diff_collection.overflow?
+ # Set our state to 'overflow' to make the #empty? and #collected?
+ # methods (generated by StateMachine) return false.
+ self.state = :overflow
end
- if new_diffs.sum { |diff| diff.diff.lines.count } > Commit::DIFF_HARD_LIMIT_LINES
- self.state = :overflow_diff_lines_limit
- new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES)
- end
- end
+ self.real_size = diff_collection.real_size
- if new_diffs.present?
- new_diffs = dump_commits(new_diffs)
- self.state = :collected
+ if diff_collection.any?
+ new_diffs = dump_diffs(diff_collection)
+ self.state = :collected
+ end
end
self.st_diffs = new_diffs
+
+ self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch)
+
self.save
end
# Collect array of Git::Diff objects
# between target and source branches
def unmerged_diffs
- compare_result.diffs || []
- rescue Gitlab::Git::Diff::TimeoutError
- self.state = :timeout
- []
+ compare.diffs(Commit.max_diff_options)
end
def repository
merge_request.target_project.repository
end
- private
+ def source_sha
+ source_commit = merge_request.source_project.commit(source_branch)
+ source_commit.try(:sha)
+ end
- def compare_result
- @compare_result ||=
+ def compare
+ @compare ||=
begin
# Update ref for merge request
merge_request.fetch_ref
- # Get latest sha of branch from source project
- source_commit = merge_request.source_project.commit(source_branch)
- source_sha = source_commit.try(:sha)
-
- Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- merge_request.target_project.repository.raw_repository,
- merge_request.target_branch,
- source_sha,
- )
+ Gitlab::Git::Compare.new(
+ self.repository.raw_repository,
+ self.target_branch,
+ self.source_sha
)
end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d8c7536cd31..de7183bf6b4 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -19,21 +19,25 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
+ Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId
include Sortable
+ include Referable
include StripAttribute
+ include Milestoneish
belongs_to :project
has_many :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
- has_many :participants, through: :issues, source: :assignee
+ has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
scope :of_projects, ->(ids) { where(project_id: ids) }
- validates :title, presence: true
+ validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true
strip_attributes :title
@@ -55,44 +59,60 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
class << self
+ # Searches for milestones matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- query = "%#{query}%"
- where("title like ? or description like ?", query, query)
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
end
- def expired?
- if due_date
- due_date.past?
- else
- false
- end
+ def self.reference_pattern
+ nil
+ end
+
+ def self.link_reference_pattern
+ super("milestones", /(?<milestone>\d+)/)
end
- def open_items_count
- self.issues.opened.count + self.merge_requests.opened.count
+ def self.upcoming
+ self.where('due_date > ?', Time.now).order(due_date: :asc).first
end
- def closed_items_count
- self.issues.closed.count + self.merge_requests.closed_and_merged.count
+ def to_reference(from_project = nil)
+ escaped_title = self.title.gsub("]", "\\]")
+
+ h = Gitlab::Application.routes.url_helpers
+ url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
+
+ "[#{escaped_title}](#{url})"
end
- def total_items_count
- self.issues.count + self.merge_requests.count
+ def reference_link_text(from_project = nil)
+ self.title
end
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
- rescue ZeroDivisionError
- 0
+ def expired?
+ if due_date
+ due_date.past?
+ else
+ false
+ end
end
def expires_at
if due_date
if due_date.past?
- "expired at #{due_date.stamp("Aug 21, 2011")}"
+ "expired on #{due_date.to_s(:medium)}"
else
- "expires at #{due_date.stamp("Aug 21, 2011")}"
+ "expires on #{due_date.to_s(:medium)}"
end
end
end
@@ -101,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/namespace.rb b/app/models/namespace.rb
index adafabbec07..55842df1e2d 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
class Namespace < ActiveRecord::Base
@@ -53,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end
+ # Searches for namespaces matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation
def search(query)
- where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end
def clean_path(path)
diff --git a/app/models/note.rb b/app/models/note.rb
index 3d5b663c99f..b0c33f2eec5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -33,14 +33,18 @@ class Note < ActiveRecord::Base
participant :author
belongs_to :project
- belongs_to :noteable, polymorphic: true
+ belongs_to :noteable, polymorphic: true, touch: true
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
+ has_many :todos, dependent: :destroy
+
+ delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
+ before_validation :clear_blank_line_code!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
@@ -60,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
- scope :not_inline, ->{ where(line_code: [nil, '']) }
+ scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
@@ -85,7 +89,7 @@ class Note < ActiveRecord::Base
next if discussion_ids.include?(note.discussion_id)
# don't group notes for the main target
- if !note.for_diff_line? && note.noteable_type == "MergeRequest"
+ if !note.for_diff_line? && note.for_merge_request?
discussions << [note]
else
discussions << notes.select do |other_note|
@@ -102,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end
+ # Searches for notes matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(note) like :query", query: "%#{query.downcase}%")
+ table = arel_table
+ pattern = "%#{query}%"
+
+ where(table[:note].matches(pattern))
end
def grouped_awards
@@ -129,9 +143,11 @@ class Note < ActiveRecord::Base
end
def find_diff
- return nil unless noteable && noteable.diffs.present?
+ return nil unless noteable
+ return @diff if defined?(@diff)
- @diff ||= noteable.diffs.find do |d|
+ # Don't use ||= because nil is a valid value for @diff
+ @diff = noteable.diffs(Commit.max_diff_options).find do |d|
Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path
end
end
@@ -157,30 +173,29 @@ class Note < ActiveRecord::Base
Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
end
- # Check if such line of code exists in merge request diff
- # If exists - its active discussion
- # If not - its outdated diff
+ # Check if this note is part of an "active" discussion
+ #
+ # This will always return true for anything except MergeRequest noteables,
+ # which have special logic.
+ #
+ # If the note's current diff cannot be matched in the MergeRequest's current
+ # diff, it's considered inactive.
def active?
return true unless self.diff
return false unless noteable
+ return @active if defined?(@active)
- noteable.diffs.each do |mr_diff|
- next unless mr_diff.new_path == self.diff.new_path
+ noteable_diff = find_noteable_diff
- lines = Gitlab::Diff::Parser.new.parse(mr_diff.diff.lines.to_a)
+ if noteable_diff
+ parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
- lines.each do |line|
- if line.text == diff_line
- return true
- end
- end
+ @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
+ else
+ @active = false
end
- false
- end
-
- def outdated?
- !active?
+ @active
end
def diff_file_index
@@ -244,7 +259,7 @@ class Note < ActiveRecord::Base
prev_match_line = nil
prev_lines = []
- diff_lines.each do |line|
+ highlighted_diff_lines.each do |line|
if line.type == "match"
prev_lines.clear
prev_match_line = line
@@ -261,7 +276,11 @@ class Note < ActiveRecord::Base
end
def diff_lines
- @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a)
+ @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
+ end
+
+ def highlighted_diff_lines
+ Gitlab::Diff::Highlight.new(diff_lines).highlight
end
def discussion_id
@@ -309,20 +328,6 @@ class Note < ActiveRecord::Base
nil
end
- # Mentionable override.
- def gfm_reference(from_project = nil)
- noteable.gfm_reference(from_project)
- end
-
- # Mentionable override.
- def local_reference
- noteable
- end
-
- def noteable_type_name
- noteable_type.downcase if noteable_type.present?
- end
-
# FIXME: Hack for polymorphic associations with STI
# For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
def noteable_type=(noteable_type)
@@ -342,10 +347,6 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self)
end
- def system?
- read_attribute(:system)
- end
-
def downvote?
is_award && note == "thumbsdown"
end
@@ -358,6 +359,10 @@ class Note < ActiveRecord::Base
!system? && !is_award
end
+ def cross_reference_not_visible_for?(user)
+ cross_reference? && referenced_mentionables(user).empty?
+ end
+
# Checks if note is an award added as a comment
#
# If note is an award, this method sets is_award to true
@@ -367,14 +372,25 @@ class Note < ActiveRecord::Base
#
def set_award!
return unless awards_supported? && contains_emoji_only?
+
self.is_award = true
self.note = award_emoji_name
end
private
+ def clear_blank_line_code!
+ self.line_code = nil if self.line_code.blank?
+ end
+
+ # Find the diff on noteable that matches our own
+ def find_noteable_diff
+ diffs = noteable.diffs(Commit.max_diff_options)
+ diffs.find { |d| d.new_path == self.diff.new_path }
+ end
+
def awards_supported?
- noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
+ (for_issue? || for_merge_request?) && !for_diff_line?
end
def contains_emoji_only?
diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb
index 9cee3b70cb3..452f3913eef 100644
--- a/app/models/personal_snippet.rb
+++ b/app/models/personal_snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
diff --git a/app/models/project.rb b/app/models/project.rb
index 017471995ec..412c6c6732d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -29,6 +29,14 @@
# import_source :string(255)
# commit_count :integer default(0)
# import_error :text
+# ci_id :integer
+# builds_enabled :boolean default(TRUE), not null
+# shared_runners_enabled :boolean default(TRUE), not null
+# runners_token :string
+# build_coverage_regex :string
+# build_allow_git_fetch :boolean default(TRUE), not null
+# build_timeout :integer default(3600), not null
+# pending_delete :boolean
#
require 'carrierwave/orm/activerecord'
@@ -43,6 +51,7 @@ class Project < ActiveRecord::Base
include Sortable
include AfterCommitQueue
include CaseSensitivity
+ include TokenAuthenticatable
extend Gitlab::ConfigHelper
@@ -81,6 +90,7 @@ class Project < ActiveRecord::Base
acts_as_taggable_on :tags
attr_accessor :new_default_branch
+ attr_accessor :old_path_with_namespace
# Relations
belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
@@ -141,6 +151,9 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
+ has_many :project_group_links, dependent: :destroy
+ has_many :invited_groups, through: :project_group_links, source: :group
+ has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
@@ -185,10 +198,8 @@ class Project < ActiveRecord::Base
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- before_validation :set_runners_token_token
- def set_runners_token_token
- self.runners_token = SecureRandom.hex(15) if self.runners_token.blank?
- end
+ add_authentication_token_field :runners_token
+ before_save :ensure_runners_token
mount_uploader :avatar, AvatarUploader
@@ -207,6 +218,7 @@ class Project < ActiveRecord::Base
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 }
state_machine :import_status, initial: :none do
event :import_start do
@@ -242,12 +254,6 @@ class Project < ActiveRecord::Base
where('projects.last_activity_at < ?', 6.months.ago)
end
- def publicish(user)
- visibility_levels = [Project::PUBLIC]
- visibility_levels << Project::INTERNAL if user
- where(visibility_level: visibility_levels)
- end
-
def with_push
joins(:events).where('events.action = ?', Event::PUSHED)
end
@@ -256,17 +262,49 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
+ # Searches for a list of projects based on the query given in `query`.
+ #
+ # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
+ # search. On MySQL a regular "LIKE" is used as it's already
+ # case-insensitive.
+ #
+ # query - The search query as a String.
def search(query)
- joins(:namespace).
- where('LOWER(projects.name) LIKE :query OR
- LOWER(projects.path) LIKE :query OR
- LOWER(namespaces.name) LIKE :query OR
- LOWER(projects.description) LIKE :query',
- query: "%#{query.try(:downcase)}%")
+ ptable = arel_table
+ ntable = Namespace.arel_table
+ pattern = "%#{query}%"
+
+ projects = select(:id).where(
+ ptable[:path].matches(pattern).
+ or(ptable[:name].matches(pattern)).
+ or(ptable[:description].matches(pattern))
+ )
+
+ # We explicitly remove any eager loading clauses as they're:
+ #
+ # 1. Not needed by this query
+ # 2. Combined with .joins(:namespace) lead to all columns from the
+ # projects & namespaces tables being selected, leading to a SQL error
+ # due to the columns of all UNION'd queries no longer being the same.
+ namespaces = select(:id).
+ except(:includes).
+ joins(:namespace).
+ where(ntable[:name].matches(pattern))
+
+ union = Gitlab::SQL::Union.new([projects, namespaces])
+
+ where("projects.id IN (#{union.to_sql})")
+ end
+
+ def search_by_visibility(level)
+ where(visibility_level: Gitlab::VisibilityLevel.const_get(level.upcase))
end
def search_by_title(query)
- where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%")
+ pattern = "%#{query}%"
+ table = Project.arel_table
+
+ non_archived.where(table[:name].matches(pattern))
end
def find_with_namespace(id)
@@ -330,13 +368,18 @@ class Project < ActiveRecord::Base
end
def repository
- @repository ||= Repository.new(path_with_namespace, nil, self)
+ @repository ||= Repository.new(path_with_namespace, self)
end
def commit(id = 'HEAD')
repository.commit(id)
end
+ def merge_base_commit(first_commit_id, second_commit_id)
+ sha = repository.merge_base(first_commit_id, second_commit_id)
+ repository.commit(sha) if sha
+ end
+
def saved?
id && persisted?
end
@@ -365,6 +408,10 @@ class Project < ActiveRecord::Base
external_import? || forked?
end
+ def no_import?
+ import_status == 'none'
+ end
+
def external_import?
import_url.present?
end
@@ -390,7 +437,7 @@ class Project < ActiveRecord::Base
result.password = '*****' unless result.password.nil?
result.to_s
rescue
- original_url
+ self.import_url
end
def check_limit
@@ -461,12 +508,10 @@ class Project < ActiveRecord::Base
!external_issue_tracker
end
- def external_issues_trackers
- services.select(&:issue_tracker?).reject(&:default?)
- end
-
def external_issue_tracker
- @external_issues_tracker ||= external_issues_trackers.find(&:activated?)
+ return @external_issue_tracker if defined?(@external_issue_tracker)
+ @external_issue_tracker ||=
+ services.issue_trackers.active.without_defaults.first
end
def can_have_issues_tracker_id?
@@ -508,11 +553,11 @@ class Project < ActiveRecord::Base
end
def ci_services
- services.select { |service| service.category == :ci }
+ services.where(category: :ci)
end
def ci_service
- @ci_service ||= ci_services.find(&:activated?)
+ @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
def jira_tracker?
@@ -526,10 +571,7 @@ class Project < ActiveRecord::Base
end
def avatar_in_git
- @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png')
- @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg')
- @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif')
- @avatar_file
+ repository.avatar
end
def avatar_url
@@ -693,6 +735,8 @@ class Project < ActiveRecord::Base
old_path_with_namespace = File.join(namespace_dir, path_was)
new_path_with_namespace = File.join(namespace_dir, path)
+ expire_caches_before_rename(old_path_with_namespace)
+
if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
# If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository
@@ -701,6 +745,11 @@ class Project < ActiveRecord::Base
gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
reset_events_cache
+
+ @old_path_with_namespace = old_path_with_namespace
+
+ SystemHooksService.new.execute_hooks_for(self, :rename)
+
@repository = nil
rescue
# Returning false does not rollback after_* transaction but gives
@@ -716,14 +765,39 @@ class Project < ActiveRecord::Base
Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path)
end
+ # Expires various caches before a project is renamed.
+ def expire_caches_before_rename(old_path)
+ repo = Repository.new(old_path, self)
+ wiki = Repository.new("#{old_path}.wiki", self)
+
+ if repo.exists?
+ repo.expire_cache
+ repo.expire_emptiness_caches
+ end
+
+ if wiki.exists?
+ wiki.expire_cache
+ wiki.expire_emptiness_caches
+ end
+ end
+
def hook_attrs
{
name: name,
- ssh_url: ssh_url_to_repo,
- http_url: http_url_to_repo,
+ description: description,
web_url: web_url,
+ avatar_url: avatar_url,
+ git_ssh_url: ssh_url_to_repo,
+ git_http_url: http_url_to_repo,
namespace: namespace.name,
- visibility_level: visibility_level
+ visibility_level: visibility_level,
+ path_with_namespace: path_with_namespace,
+ default_branch: default_branch,
+ # Backward compatibility
+ homepage: web_url,
+ url: url_to_repo,
+ ssh_url: ssh_url_to_repo,
+ http_url: http_url_to_repo
}
end
@@ -769,6 +843,7 @@ class Project < ActiveRecord::Base
end
def change_head(branch)
+ repository.before_change_head
gitlab_shell.update_repository_head(self.path_with_namespace, branch)
reload_default_branch
end
@@ -825,6 +900,10 @@ class Project < ActiveRecord::Base
jira_tracker? && jira_service.active
end
+ def allowed_to_share_with_group?
+ !namespace.share_with_group_lock
+ end
+
def ci_commit(sha)
ci_commits.find_by(sha: sha)
end
@@ -856,13 +935,13 @@ class Project < ActiveRecord::Base
end
def valid_runners_token? token
- self.runners_token && self.runners_token == token
+ self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
# TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token
- self.builds_enabled? && self.runners_token && self.runners_token == token
+ self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
def build_coverage_enabled?
@@ -885,4 +964,12 @@ class Project < ActiveRecord::Base
return true unless forked?
Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
end
+
+ def runners_token
+ ensure_runners_token!
+ end
+
+ def wiki
+ @wiki ||= ProjectWiki.new(self, self.owner)
+ end
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
new file mode 100644
index 00000000000..e52a6bd7c84
--- /dev/null
+++ b/app/models/project_group_link.rb
@@ -0,0 +1,36 @@
+class ProjectGroupLink < ActiveRecord::Base
+ GUEST = 10
+ REPORTER = 20
+ DEVELOPER = 30
+ MASTER = 40
+
+ belongs_to :project
+ belongs_to :group
+
+ validates :project_id, presence: true
+ validates :group_id, presence: true
+ validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
+ validates :group_access, presence: true
+ validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
+ validate :different_group
+
+ def self.access_options
+ Gitlab::Access.options
+ end
+
+ def self.default_access
+ DEVELOPER
+ end
+
+ def human_access
+ self.class.access_options.key(self.group_access)
+ end
+
+ private
+
+ def different_group
+ if self.group && self.project && self.project.group == self.group
+ errors.add(:base, "Project cannot be shared with the project it is in.")
+ end
+ end
+end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index e6e16058d41..792ad804575 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -16,7 +16,9 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
+
require 'asana'
class AsanaService < Service
@@ -40,8 +42,8 @@ get the commit comment added to it.
You can also close a task with a message containing: `fix #123456`.
-You can find your Api Keys here:
-http://developer.asana.com/documentation/#api_keys'
+You can create a Personal Access Token here:
+http://app.asana.com/-/account_api'
end
def to_param
@@ -53,14 +55,12 @@ http://developer.asana.com/documentation/#api_keys'
{
type: 'text',
name: 'api_key',
- placeholder: 'User API token. User must have access to task,
-all comments will be attributed to this user.'
+ placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.'
},
{
type: 'text',
name: 'restrict_to_branch',
- placeholder: 'Comma-separated list of branches which will be
-automatically inspected. Leave blank to include all branches.'
+ placeholder: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
]
end
@@ -69,58 +69,58 @@ automatically inspected. Leave blank to include all branches.'
%w(push)
end
+ def client
+ @_client ||= begin
+ Asana::Client.new do |c|
+ c.authentication :access_token, api_key
+ end
+ end
+ end
+
def execute(data)
return unless supported_events.include?(data[:object_kind])
- Asana.configure do |client|
- client.api_key = api_key
- end
-
- user = data[:user_name]
+ # check the branch restriction is poplulated and branch is not included
branch = Gitlab::Git.ref_name(data[:ref])
-
branch_restriction = restrict_to_branch.to_s
-
- # check the branch restriction is poplulated and branch is not included
if branch_restriction.length > 0 && branch_restriction.index(branch).nil?
return
end
+ user = data[:user_name]
project_name = project.name_with_namespace
- push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name
data[:commits].each do |commit|
- check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg)
+ push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):"
+ check_commit(commit[:message], push_msg)
end
end
def check_commit(message, push_msg)
- task_list = []
- close_list = []
-
- message.split("\n").each do |line|
- # look for a task ID or a full Asana url
- task_list.concat(line.scan(/#(\d+)/))
- task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/))
- # look for a word starting with 'fix' followed by a task ID
- close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i))
- end
-
- # post commit to every taskid found
- task_list.each do |taskid|
- task = Asana::Task.find(taskid[0])
-
- if task
- task.create_story(text: push_msg + ' ' + message)
- end
- end
-
- # close all tasks that had 'fix(ed/es/ing) #:id' in them
- close_list.each do |taskid|
- task = Asana::Task.find(taskid.last)
-
- if task
- task.modify(completed: true)
+ # matches either:
+ # - #1234
+ # - https://app.asana.com/0/0/1234
+ # optionally preceded with:
+ # - fix/ed/es/ing
+ # - close/s/d
+ # - closing
+ issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i
+
+ message.scan(issue_finder).each do |tuple|
+ # tuple will be
+ # [ 'fix', 'id_from_url', 'id_from_pound' ]
+ taskid = tuple[2] || tuple[1]
+
+ begin
+ task = Asana::Task.find_by_id(client, taskid)
+ task.add_comment(text: "#{push_msg} #{message}")
+
+ if tuple[0]
+ task.update(completed: true)
+ end
+ rescue => e
+ Rails.logger.error(e.message)
+ next
end
end
end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index fb7e0c0fb0d..29d841faed8 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class AssemblaService < Service
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index aa8746beb80..9e7f642180e 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class BambooService < CiService
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 199ee3a9d0d..3efbfd2eec3 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "addressable/uri"
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 8247c79fc33..f6313255cbb 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class BuildsEmailService < Service
@@ -72,12 +73,16 @@ class BuildsEmailService < Service
when 'success'
!notify_only_broken_builds?
when 'failed'
- true
+ !allow_failure?(data)
else
false
end
end
+ def allow_failure?(data)
+ data[:build_allow_failure] == true
+ end
+
def all_recipients(data)
all_recipients = recipients.split(',')
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index e591afdda64..6e8f0842524 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class CampfireService < Service
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 88186113c68..d9f0849d147 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -16,20 +16,19 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# Base class for CI services
# List methods you need to implement to get your CI service
# working with GitLab Merge Requests
class CiService < Service
- def category
- :ci
- end
-
+ default_value_for :category, 'ci'
+
def valid_token?(token)
- self.respond_to?(:token) && self.token.present? && self.token == token
+ self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
-
+
def supported_events
%w(push)
end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 7c2027c18e6..88a3e9218cb 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class CustomIssueTrackerService < IssueTrackerService
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 08e5ccb3855..b4724bb647e 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class DroneCiService < CiService
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index 8f5d8b086eb..b831577cd97 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class EmailsOnPushService < Service
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 74c57949b4d..b402b68665a 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class ExternalWikiService < Service
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 15c7c907f7e..8605ce66e48 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "flowdock-git-hook"
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 202fee042e3..61babe9cfe5 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "gemnasium/gitlab_service"
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index b64d97ce75d..33f0d7ea01a 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 9558292fea3..05436cd0f79 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class GitlabIssueTrackerService < IssueTrackerService
@@ -23,9 +24,7 @@ class GitlabIssueTrackerService < IssueTrackerService
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
- def default?
- true
- end
+ default_value_for :default, true
def to_param
'gitlab'
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 1e1686a11c6..0e3fa4a40fe 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class HipchatService < Service
@@ -119,13 +120,13 @@ class HipchatService < Service
message << "#{push[:user_name]} "
if Gitlab::Git.blank_ref?(before)
message << "pushed new #{ref_type} <a href=\""\
- "#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"\
+ "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\
" to #{project_link}\n"
elsif Gitlab::Git.blank_ref?(after)
message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n"
else
message << "pushed to #{ref_type} <a href=\""\
- "#{project.web_url}/commits/#{URI.escape(ref)}\">#{ref}</a> "
+ "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> "
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
@@ -254,8 +255,8 @@ class HipchatService < Service
status = data[:commit][:status]
duration = data[:commit][:duration]
- branch_link = "<a href=\"#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"
- commit_link = "<a href=\"#{project_url}/commit/#{URI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+ branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
+ commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index d24aa317cf3..04c714bfaad 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require 'uri'
@@ -72,9 +73,10 @@ class IrkerService < Service
'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
'you want the channel to be a nickname instead, append ",isnick" to ' \
'the channel name; if the channel is protected by a secret password, ' \
- ' append "?key=secretpassword" to the URI. Note that if you specify a ' \
- ' default IRC URI to prepend before each recipient, you can just give ' \
- ' a channel name.' },
+ ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \
+ ' want to use a password, you have to omit the "#" on the channel). If you ' \
+ ' specify a default IRC URI to prepend before each recipient, you can just ' \
+ ' give a channel name.' },
{ type: 'checkbox', name: 'colorize_messages' },
]
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 936e574cccd..25045224ce5 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -16,18 +16,17 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class IssueTrackerService < Service
validates :project_url, :issues_url, :new_issue_url, presence: true, if: :activated?
- def category
- :issue_tracker
- end
+ default_value_for :category, 'issue_tracker'
def default?
- false
+ default
end
def issue_url(iid)
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index e216f406e1c..aba37921c09 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class JiraService < IssueTrackerService
@@ -39,15 +40,10 @@ class JiraService < IssueTrackerService
end
def help
- line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
+ 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
'allow a user to easily navigate to the Jira issue tracker. See the '\
'[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
'for details.'
-
- line2 = 'Support for referencing commits and automatic closing of Jira issues directly '\
- 'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)'
-
- [line1, line2].join("\n\n")
end
def title
@@ -112,7 +108,8 @@ class JiraService < IssueTrackerService
},
entity: {
name: noteable_name.humanize.downcase,
- url: entity_url
+ url: entity_url,
+ title: noteable.title
}
}
@@ -120,6 +117,7 @@ class JiraService < IssueTrackerService
end
def test_settings
+ return unless api_url.present?
result = JiraService.get(
jira_api_test_url,
headers: {
@@ -199,10 +197,11 @@ class JiraService < IssueTrackerService
user_url = data[:user][:url]
entity_name = data[:entity][:name]
entity_url = data[:entity][:url]
+ entity_title = data[:entity][:title]
project_name = data[:project][:name]
message = {
- body: "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]."
+ body: %Q{[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'}
}
unless existing_comment?(issue_name, message[:body])
@@ -217,6 +216,7 @@ class JiraService < IssueTrackerService
end
def send_message(url, message)
+ return unless api_url.present?
result = JiraService.post(
url,
body: message,
@@ -242,6 +242,7 @@ class JiraService < IssueTrackerService
end
def existing_comment?(issue_name, new_comment)
+ return unless api_url.present?
result = JiraService.get(
comment_url(issue_name),
headers: {
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index ade9ee97873..c9a890c7e3f 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class PivotaltrackerService < Service
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 53edf522e9a..e76d9eca2ab 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class PushoverService < Service
@@ -111,7 +112,7 @@ class PushoverService < Service
priority: priority,
title: "#{project.name_with_namespace}",
message: message,
- url: data[:repository][:homepage],
+ url: data[:project][:web_url],
url_title: "See project #{project.name_with_namespace}"
}
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index dd9ba97ee1f..de974354c77 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class RedmineService < IssueTrackerService
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 375b4534d07..d89cf6d17b2 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class SlackService < Service
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index a63700693d7..b8e9416131a 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class TeamcityService < CiService
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 9e2c1b0e18e..1f7d85a5f3d 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet
# Scopes
scope :fresh, -> { order("created_at DESC") }
- scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
- scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 9f380a382cb..70a8bbaba65 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -136,7 +136,7 @@ class ProjectTeam
end
def human_max_access(user_id)
- Gitlab::Access.options.key max_member_access(user_id)
+ Gitlab::Access.options_with_owner.key(max_member_access(user_id))
end
# This method assumes project and group members are eager loaded for optimal
@@ -160,7 +160,27 @@ class ProjectTeam
end
end
- access.max
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ access << max_invited_level(user_id)
+ end
+
+ access.compact.max
+ end
+
+
+ def max_invited_level(user_id)
+ project.project_group_links.map do |group_link|
+ invited_group = group_link.group
+ access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
+
+ # If group member has higher access level we should restrict it
+ # to max allowed access level
+ if access && access > group_link.group_access
+ access = group_link.group_access
+ end
+
+ access
+ end.compact.max
end
private
@@ -168,6 +188,35 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
+ invited_members = []
+
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ project.project_group_links.each do |group_link|
+ invited_group = group_link.group
+ im = invited_group.group_members
+
+ if level
+ int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+ # Skip group members if we ask for masters
+ # but max group access is developers
+ next if int_level > group_link.group_access
+
+ # If we ask for developers and max
+ # group access is developers we need to provide
+ # both group master, developers as devs
+ if int_level == group_link.group_access
+ im.where("access_level >= ?)", group_link.group_access)
+ else
+ im.send(level)
+ end
+ end
+
+ invited_members << im
+ end
+
+ invited_members = invited_members.flatten.compact
+ end
if level
project_members = project_members.send(level)
@@ -175,6 +224,7 @@ class ProjectTeam
end
user_ids = project_members.pluck(:user_id)
+ user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index b5fec38378b..59b1b86d1fb 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -2,7 +2,7 @@ class ProjectWiki
include Gitlab::ShellAdapter
MARKUPS = {
- 'Markdown' => :md,
+ 'Markdown' => :markdown,
'RDoc' => :rdoc,
'AsciiDoc' => :asciidoc
} unless defined?(MARKUPS)
@@ -12,6 +12,7 @@ class ProjectWiki
# Returns a string describing what went wrong after
# an operation fails.
attr_reader :error_message
+ attr_reader :project
def initialize(project, user = nil)
@project = project
@@ -38,11 +39,15 @@ class ProjectWiki
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
end
+ def wiki_base_path
+ ["/", @project.path_with_namespace, "/wikis"].join('')
+ end
+
# Returns the Gollum::Wiki object.
def wiki
@wiki ||= begin
Gollum::Wiki.new(path_to_repo)
- rescue Gollum::NoSuchPathError
+ rescue Rugged::OSError
create_repo!
end
end
@@ -85,7 +90,7 @@ class ProjectWiki
def create_page(title, content, format = :markdown, message = nil)
commit = commit_details(:created, message, title)
- wiki.write_page(title, format, content, commit)
+ wiki.write_page(title, format.to_sym, content, commit)
update_project_activity
rescue Gollum::DuplicatePageError => e
@@ -96,7 +101,7 @@ class ProjectWiki
def update_page(page, content, format = :markdown, message = nil)
commit = commit_details(:updated, message, page.title)
- wiki.update_page(page, page.name, format, content, commit)
+ wiki.update_page(page, page.name, format.to_sym, content, commit)
update_project_activity
end
@@ -118,7 +123,7 @@ class ProjectWiki
end
def repository
- Repository.new(path_with_namespace, default_branch, @project)
+ Repository.new(path_with_namespace, @project)
end
def default_branch
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a9bf4eb4033..25d24493f6e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -3,6 +3,10 @@ require 'securerandom'
class Repository
class CommitError < StandardError; end
+ # Files to use as a project avatar in case no avatar was uploaded via the web
+ # UI.
+ AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
+
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
@@ -15,7 +19,7 @@ class Repository
Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
end
- def initialize(path_with_namespace, default_branch = nil, project = nil)
+ def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
end
@@ -23,13 +27,11 @@ class Repository
def raw_repository
return nil unless path_with_namespace
- @raw_repository ||= begin
- repo = Gitlab::Git::Repository.new(path_to_repo)
- repo.autocrlf = :input
- repo
- rescue Gitlab::Git::Repository::NoRepository
- nil
- end
+ @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
+ end
+
+ def update_autocrlf_option
+ raw_repository.autocrlf = :input if raw_repository.autocrlf != :input
end
# Return absolute path to repository
@@ -40,11 +42,18 @@ class Repository
end
def exists?
- raw_repository
+ return false unless raw_repository
+
+ raw_repository.rugged
+ true
+ rescue Gitlab::Git::Repository::NoRepository
+ false
end
def empty?
- raw_repository.empty?
+ return @empty unless @empty.nil?
+
+ @empty = cache.fetch(:empty?) { raw_repository.empty? }
end
#
@@ -57,11 +66,15 @@ class Repository
# This method return true if repository contains some content visible in project page.
#
def has_visible_content?
- !raw_repository.branches.empty?
+ return @has_visible_content unless @has_visible_content.nil?
+
+ @has_visible_content = cache.fetch(:has_visible_content?) do
+ raw_repository.branch_count > 0
+ end
end
def commit(id = 'HEAD')
- return nil unless raw_repository
+ return nil unless exists?
commit = Gitlab::Git::Commit.find(raw_repository, id)
commit = Commit.new(commit, @project) if commit
commit
@@ -78,7 +91,8 @@ class Repository
offset: offset,
# --follow doesn't play well with --skip. See:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
- follow: false
+ follow: false,
+ skip_merges: skip_merges
}
commits = Gitlab::Git::Commit.where(options)
@@ -92,9 +106,12 @@ class Repository
commits
end
- def find_commits_by_message(query)
+ def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
+ ref ||= root_ref
+
# Limited to 1000 commits for now, could be parameterized?
- args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query})
+ args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query})
+ args = args.concat(%W(-- #{path})) if path.present?
git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
commits = git_log_results.map { |c| commit(c) }
@@ -120,18 +137,18 @@ class Repository
rugged.branches.create(branch_name, target)
end
- expire_branches_cache
+ after_create_branch
find_branch(branch_name)
end
def add_tag(tag_name, ref, message = nil)
- expire_tags_cache
+ before_push_tag
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
def rm_branch(user, branch_name)
- expire_branches_cache
+ before_remove_branch
branch = find_branch(branch_name)
oldrev = branch.try(:target)
@@ -142,12 +159,12 @@ class Repository
rugged.branches.delete(branch_name)
end
- expire_branches_cache
+ after_remove_branch
true
end
def rm_tag(tag_name)
- expire_tags_cache
+ before_remove_tag
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
@@ -170,12 +187,35 @@ class Repository
end
end
+ def branch_count
+ @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count }
+ end
+
+ def tag_count
+ @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
+ end
+
# Return repo size in megabytes
# Cached in redis
def size
cache.fetch(:size) { raw_repository.size }
end
+ def diverging_commit_counts(branch)
+ root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
+ cache.fetch(:"diverging_commit_counts_#{branch.name}") do
+ # Rugged seems to throw a `ReferenceError` when given branch_names rather
+ # than SHA-1 hashes
+ number_commits_behind = raw_repository.
+ count_commits_between(branch.target, root_ref_hash)
+
+ number_commits_ahead = raw_repository.
+ count_commits_between(root_ref_hash, branch.target)
+
+ { behind: number_commits_behind, ahead: number_commits_ahead }
+ end
+ end
+
def cache_keys
%i(size branch_names tag_names commit_count
readme version contribution_guide changelog license)
@@ -199,19 +239,62 @@ class Repository
@branches = nil
end
- def expire_cache
+ def expire_cache(branch_name = nil, revision = nil)
cache_keys.each do |key|
cache.expire(key)
end
+
+ expire_branch_cache(branch_name)
+ expire_avatar_cache(branch_name, revision)
+
+ # This ensures this particular cache is flushed after the first commit to a
+ # new repository.
+ expire_emptiness_caches if empty?
end
- def rebuild_cache
- cache_keys.each do |key|
- cache.expire(key)
- send(key)
+ def expire_branch_cache(branch_name = nil)
+ # When we push to the root branch we have to flush the cache for all other
+ # branches as their statistics are based on the commits relative to the
+ # root branch.
+ if !branch_name || branch_name == root_ref
+ branches.each do |branch|
+ cache.expire(:"diverging_commit_counts_#{branch.name}")
+ end
+ # In case a commit is pushed to a non-root branch we only have to flush the
+ # cache for said branch.
+ else
+ cache.expire(:"diverging_commit_counts_#{branch_name}")
end
end
+ def expire_root_ref_cache
+ cache.expire(:root_ref)
+ @root_ref = nil
+ end
+
+ # Expires the cache(s) used to determine if a repository is empty or not.
+ def expire_emptiness_caches
+ cache.expire(:empty?)
+ @empty = nil
+
+ expire_has_visible_content_cache
+ end
+
+ def expire_has_visible_content_cache
+ cache.expire(:has_visible_content?)
+ @has_visible_content = nil
+ end
+
+ def expire_branch_count_cache
+ cache.expire(:branch_count)
+ @branch_count = nil
+ end
+
+ def expire_tag_count_cache
+ cache.expire(:tag_count)
+ @tag_count = nil
+ end
+
def lookup_cache
@lookup_cache ||= {}
end
@@ -220,6 +303,80 @@ class Repository
cache.expire(:branch_names)
end
+ def expire_avatar_cache(branch_name = nil, revision = nil)
+ # Avatars are pulled from the default branch, thus if somebody pushes to a
+ # different branch there's no need to expire anything.
+ return if branch_name && branch_name != root_ref
+
+ # We don't want to flush the cache if the commit didn't actually make any
+ # changes to any of the possible avatar files.
+ if revision && commit = self.commit(revision)
+ return unless commit.diffs.
+ any? { |diff| AVATAR_FILES.include?(diff.new_path) }
+ end
+
+ cache.expire(:avatar)
+
+ @avatar = nil
+ end
+
+ # Runs code just before a repository is deleted.
+ def before_delete
+ expire_cache if exists?
+
+ expire_root_ref_cache
+ expire_emptiness_caches
+ end
+
+ # Runs code just before the HEAD of a repository is changed.
+ def before_change_head
+ # Cached divergent commit counts are based on repository head
+ expire_branch_cache
+ expire_root_ref_cache
+ end
+
+ # Runs code before pushing (= creating or removing) a tag.
+ def before_push_tag
+ expire_cache
+ expire_tags_cache
+ expire_tag_count_cache
+ end
+
+ # Runs code before removing a tag.
+ def before_remove_tag
+ expire_tags_cache
+ expire_tag_count_cache
+ end
+
+ # Runs code after a repository has been forked/imported.
+ def after_import
+ expire_emptiness_caches
+ end
+
+ # Runs code after a new commit has been pushed.
+ def after_push_commit(branch_name, revision)
+ expire_cache(branch_name, revision)
+ end
+
+ # Runs code after a new branch has been created.
+ def after_create_branch
+ expire_branches_cache
+ expire_has_visible_content_cache
+ expire_branch_count_cache
+ end
+
+ # Runs code before removing an existing branch.
+ def before_remove_branch
+ expire_branches_cache
+ end
+
+ # Runs code after an existing branch has been removed.
+ def after_remove_branch
+ expire_has_visible_content_cache
+ expire_branch_count_cache
+ expire_branches_cache
+ end
+
def method_missing(m, *args, &block)
if m == :lookup && !block_given?
lookup_cache[m] ||= {}
@@ -437,7 +594,7 @@ class Repository
end
def root_ref
- @root_ref ||= raw_repository.root_ref
+ @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
end
def commit_dir(user, path, message, branch)
@@ -536,6 +693,42 @@ class Repository
end
end
+ def revert(user, commit, base_branch, revert_tree_id = nil)
+ source_sha = find_branch(base_branch).target
+ revert_tree_id ||= check_revert_content(commit, base_branch)
+
+ return false unless revert_tree_id
+
+ commit_with_hooks(user, base_branch) do |ref|
+ committer = user_to_committer(user)
+ source_sha = Rugged::Commit.create(rugged,
+ message: commit.revert_message,
+ author: committer,
+ committer: committer,
+ tree: revert_tree_id,
+ parents: [rugged.lookup(source_sha)],
+ update_ref: ref)
+ end
+ end
+
+ def check_revert_content(commit, base_branch)
+ source_sha = find_branch(base_branch).target
+ args = [commit.id, source_sha]
+ args << { mainline: 1 } if commit.merge_commit?
+
+ revert_index = rugged.revert_commit(*args)
+ return false if revert_index.conflicts?
+
+ tree_id = revert_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
+ end
+
+ def diff_exists?(sha1, sha2)
+ rugged.diff(sha1, sha2).size > 0
+ end
+
def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
@@ -548,7 +741,11 @@ class Repository
end
def merge_base(first_commit_id, second_commit_id)
+ first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
+ second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
rugged.merge_base(first_commit_id, second_commit_id)
+ rescue Rugged::ReferenceError
+ nil
end
def is_ancestor?(ancestor_id, descendant_id)
@@ -558,19 +755,22 @@ class Repository
def search_files(query, ref)
offset = 2
- args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
+ args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
def parse_search_result(result)
ref = nil
filename = nil
+ basename = nil
startline = 0
result.each_line.each_with_index do |line, index|
if line =~ /^.*:.*:\d+:/
ref, filename, startline = line.split(':')
startline = startline.to_i - index
+ extname = File.extname(filename)
+ basename = filename.sub(/#{extname}$/, '')
break
end
end
@@ -583,6 +783,7 @@ class Repository
OpenStruct.new(
filename: filename,
+ basename: basename,
ref: ref,
startline: startline,
data: data
@@ -609,12 +810,15 @@ class Repository
end
def commit_with_hooks(current_user, branch)
+ update_autocrlf_option
+
oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
+ target_branch = find_branch(branch)
was_empty = empty?
- unless was_empty
- oldrev = find_branch(branch).target
+ if !was_empty && target_branch
+ oldrev = target_branch.target
end
with_tmp_ref(oldrev) do |tmp_ref|
@@ -626,7 +830,7 @@ class Repository
end
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
- if was_empty
+ if was_empty || !target_branch
# Create branch
rugged.references.create(ref, newrev)
else
@@ -641,6 +845,27 @@ class Repository
end
end
end
+
+ newrev
+ end
+ end
+
+ def ls_files(ref)
+ actual_ref = ref || root_ref
+ raw_repository.ls_files(actual_ref)
+ end
+
+ def main_language
+ unless empty?
+ Linguist::Repository.new(rugged, rugged.head.target_id).language
+ end
+ end
+
+ def avatar
+ @avatar ||= cache.fetch(:avatar) do
+ AVATAR_FILES.find do |file|
+ blob_at_branch('master', file)
+ end
end
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index f36eda1531b..77115597d71 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -25,8 +25,6 @@ class SentNotification < ActiveRecord::Base
class << self
def reply_key
- return nil unless Gitlab::IncomingEmail.enabled?
-
SecureRandom.hex(16)
end
@@ -59,11 +57,15 @@ class SentNotification < ActiveRecord::Base
def record_note(note, recipient_id, reply_key, params = {})
params[:line_code] = note.line_code
-
+
record(note.noteable, recipient_id, reply_key, params)
end
end
+ def unsubscribable?
+ !for_commit?
+ end
+
def for_commit?
noteable_type == "Commit"
end
@@ -75,4 +77,8 @@ class SentNotification < ActiveRecord::Base
super
end
end
+
+ def to_param
+ self.reply_key
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index d3bf7f0ebd1..721273250ea 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# To add new service you should build a class inherited from Service
@@ -42,6 +43,9 @@ class Service < ActiveRecord::Base
validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) }
+ scope :issue_trackers, -> { where(category: 'issue_tracker') }
+ scope :active, -> { where(active: true) }
+ scope :without_defaults, -> { where(default: false) }
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
@@ -50,6 +54,8 @@ class Service < ActiveRecord::Base
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
+ default_value_for :category, 'common'
+
def activated?
active
end
@@ -59,7 +65,7 @@ class Service < ActiveRecord::Base
end
def category
- :common
+ read_attribute(:category).to_sym
end
def initialize_properties
@@ -152,7 +158,7 @@ class Service < ActiveRecord::Base
# Returns a hash of the properties that have been assigned a new value since last save,
# indicating their original values (attr => original value).
- # ActiveRecord does not provide a mechanism to track changes in serialized keys,
+ # ActiveRecord does not provide a mechanism to track changes in serialized keys,
# so we need a specific implementation for service properties.
# This allows to track changes to properties set with the accessor methods,
# but not direct manipulation of properties hash.
@@ -163,7 +169,7 @@ class Service < ActiveRecord::Base
def reset_updated_properties
@updated_properties = nil
end
-
+
def async_execute(data)
return unless supported_events.include?(data[:object_kind])
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index f876be7a4c8..b9e835a4486 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
- scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
- scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
participant :author, :notes
@@ -111,21 +108,37 @@ class Snippet < ActiveRecord::Base
nil
end
- def expired?
- expires_at && expires_at < Time.current
- end
-
def visibility_level_field
visibility_level
end
class << self
+ # Searches for snippets with a matching title or file name.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end
+ # Searches for snippets with matching content.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search_code(query)
- where('(content LIKE :query)', query: "%#{query}%")
+ table = Snippet.arel_table
+ pattern = "%#{query}%"
+
+ where(table[:content].matches(pattern))
end
def accessible_to(user)
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
new file mode 100644
index 00000000000..12df68ef83b
--- /dev/null
+++ b/app/models/spam_log.rb
@@ -0,0 +1,10 @@
+class SpamLog < ActiveRecord::Base
+ belongs_to :user
+
+ validates :user, presence: true
+
+ def remove_user
+ user.block
+ user.destroy
+ end
+end
diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb
new file mode 100644
index 00000000000..cdc7321b08e
--- /dev/null
+++ b/app/models/spam_report.rb
@@ -0,0 +1,5 @@
+class SpamReport < ActiveRecord::Base
+ belongs_to :user
+
+ validates :user, presence: true
+end
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index dd75d3ab8ba..dd800ce110f 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -15,7 +15,7 @@ class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :subscribable, polymorphic: true
- validates :user_id,
+ validates :user_id,
uniqueness: { scope: [:subscribable_id, :subscribable_type] },
presence: true
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
new file mode 100644
index 00000000000..5f91991f781
--- /dev/null
+++ b/app/models/todo.rb
@@ -0,0 +1,53 @@
+# == Schema Information
+#
+# Table name: todos
+#
+# id :integer not null, primary key
+# user_id :integer not null
+# project_id :integer not null
+# target_id :integer not null
+# 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
+#
+
+class Todo < ActiveRecord::Base
+ ASSIGNED = 1
+ MENTIONED = 2
+
+ belongs_to :author, class_name: "User"
+ belongs_to :note
+ belongs_to :project
+ belongs_to :target, polymorphic: true, touch: true
+ belongs_to :user
+
+ delegate :name, :email, to: :author, prefix: true, allow_nil: true
+
+ validates :action, :project, :target, :user, presence: true
+
+ default_scope { reorder(id: :desc) }
+
+ scope :pending, -> { with_state(:pending) }
+ scope :done, -> { with_state(:done) }
+
+ state_machine :state, initial: :pending do
+ event :done do
+ transition [:pending, :done] => :done
+ end
+
+ state :pending
+ state :done
+ end
+
+ def body
+ if note.present?
+ note.note
+ else
+ target.title
+ end
+ end
+end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 93b3246a668..7c4ed6e393b 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -19,20 +19,28 @@ class Tree
available_readmes = blobs.select(&:readme?)
- if available_readmes.count == 0
- return @readme = nil
+ previewable_readmes = available_readmes.select do |blob|
+ previewable?(blob.name)
+ end
+
+ plain_readmes = available_readmes.select do |blob|
+ plain?(blob.name)
end
- # Take the first previewable readme, or the first available readme, if we
- # can't preview any of them
- readme_tree = available_readmes.find do |readme|
- previewable?(readme.name)
- end || available_readmes.first
+ # Prioritize previewable over plain readmes
+ readme_tree = previewable_readmes.first || plain_readmes.first
+
+ # Return if we can't preview any of them
+ if readme_tree.nil?
+ return @readme = nil
+ end
readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
git_repo = repository.raw_repository
@readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path)
+ @readme.load_all_data!(git_repo)
+ @readme
end
def trees
diff --git a/app/models/user.rb b/app/models/user.rb
index df87f3b79bd..c011af03591 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,62 +2,64 @@
#
# Table name: users
#
-# id :integer not null, primary key
-# email :string(255) default(""), not null
-# encrypted_password :string(255) default(""), not null
-# reset_password_token :string(255)
-# reset_password_sent_at :datetime
-# remember_created_at :datetime
-# sign_in_count :integer default(0)
-# current_sign_in_at :datetime
-# last_sign_in_at :datetime
-# current_sign_in_ip :string(255)
-# last_sign_in_ip :string(255)
-# created_at :datetime
-# updated_at :datetime
-# name :string(255)
-# admin :boolean default(FALSE), not null
-# projects_limit :integer default(10)
-# skype :string(255) default(""), not null
-# linkedin :string(255) default(""), not null
-# twitter :string(255) default(""), not null
-# authentication_token :string(255)
-# theme_id :integer default(1), not null
-# bio :string(255)
-# failed_attempts :integer default(0)
-# locked_at :datetime
-# unlock_token :string(255)
-# username :string(255)
-# can_create_group :boolean default(TRUE), not null
-# can_create_team :boolean default(TRUE), not null
-# state :string(255)
-# color_scheme_id :integer default(1), not null
-# notification_level :integer default(1), not null
-# password_expires_at :datetime
-# created_by_id :integer
-# last_credential_check_at :datetime
-# avatar :string(255)
-# confirmation_token :string(255)
-# confirmed_at :datetime
-# confirmation_sent_at :datetime
-# unconfirmed_email :string(255)
-# hide_no_ssh_key :boolean default(FALSE)
-# website_url :string(255) default(""), not null
-# notification_email :string(255)
-# hide_no_password :boolean default(FALSE)
-# password_automatically_set :boolean default(FALSE)
-# location :string(255)
-# encrypted_otp_secret :string(255)
-# encrypted_otp_secret_iv :string(255)
-# encrypted_otp_secret_salt :string(255)
-# otp_required_for_login :boolean default(FALSE), not null
-# otp_backup_codes :text
-# public_email :string(255) default(""), not null
-# dashboard :integer default(0)
-# project_view :integer default(0)
-# consumed_timestep :integer
-# layout :integer default(0)
-# hide_project_limit :boolean default(FALSE)
+# id :integer not null, primary key
+# email :string(255) default(""), not null
+# encrypted_password :string(255) default(""), not null
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime
+# updated_at :datetime
+# name :string(255)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
+# skype :string(255) default(""), not null
+# linkedin :string(255) default(""), not null
+# twitter :string(255) default(""), not null
+# authentication_token :string(255)
+# theme_id :integer default(1), not null
+# bio :string(255)
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# username :string(255)
+# can_create_group :boolean default(TRUE), not null
+# can_create_team :boolean default(TRUE), not null
+# state :string(255)
+# color_scheme_id :integer default(1), not null
+# notification_level :integer default(1), not null
+# password_expires_at :datetime
+# created_by_id :integer
+# last_credential_check_at :datetime
+# avatar :string(255)
+# confirmation_token :string(255)
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string(255)
+# hide_no_ssh_key :boolean default(FALSE)
+# website_url :string(255) default(""), not null
+# notification_email :string(255)
+# hide_no_password :boolean default(FALSE)
+# password_automatically_set :boolean default(FALSE)
+# location :string(255)
+# encrypted_otp_secret :string(255)
+# encrypted_otp_secret_iv :string(255)
+# encrypted_otp_secret_salt :string(255)
+# otp_required_for_login :boolean default(FALSE), not null
+# otp_backup_codes :text
+# public_email :string(255) default(""), not null
+# dashboard :integer default(0)
+# project_view :integer default(0)
+# consumed_timestep :integer
+# layout :integer default(0)
+# hide_project_limit :boolean default(FALSE)
+# unlock_token :string
+# otp_grace_period_started_at :datetime
+# external :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -76,6 +78,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :authentication_token
default_value_for :admin, false
+ default_value_for :external, false
default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
@@ -137,18 +140,16 @@ class User < ActiveRecord::Base
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
has_one :abuse_report, dependent: :destroy
+ has_many :spam_logs, dependent: :destroy
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
-
+ has_many :todos, dependent: :destroy
#
# Validations
#
validates :name, presence: true
- # Note that a 'uniqueness' and presence check is provided by devise :validatable for email. We do not need to
- # duplicate that here as the validation framework will have duplicate errors in the event of a failure.
- validates :email, presence: true, email: { strict_mode: true }
- validates :notification_email, presence: true, email: { strict_mode: true }
- validates :public_email, presence: true, email: { strict_mode: true }, allow_blank: true, uniqueness: true
+ validates :notification_email, presence: true, email: true
+ validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :username,
@@ -172,6 +173,7 @@ class User < ActiveRecord::Base
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token
+ before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
after_create :post_create_hook
@@ -195,10 +197,22 @@ class User < ActiveRecord::Base
state_machine :state, initial: :active do
event :block do
transition active: :blocked
+ transition ldap_blocked: :blocked
+ end
+
+ event :ldap_block do
+ transition active: :ldap_blocked
end
event :activate do
transition blocked: :active
+ transition ldap_blocked: :active
+ end
+
+ state :blocked, :ldap_blocked do
+ def blocked?
+ true
+ end
end
end
@@ -206,7 +220,8 @@ class User < ActiveRecord::Base
# Scopes
scope :admins, -> { where(admin: true) }
- scope :blocked, -> { with_state(:blocked) }
+ scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
+ scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
@@ -262,13 +277,29 @@ class User < ActiveRecord::Base
self.with_two_factor
when 'wop'
self.without_projects
+ when 'external'
+ self.external
else
self.active
end
end
+ # Searches users matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%")
+ table = arel_table
+ pattern = "%#{query}%"
+
+ where(
+ table[:name].matches(pattern).
+ or(table[:email].matches(pattern)).
+ or(table[:username].matches(pattern))
+ )
end
def by_login(login)
@@ -343,19 +374,24 @@ class User < ActiveRecord::Base
def disable_two_factor!
update_attributes(
- two_factor_enabled: false,
- encrypted_otp_secret: nil,
- encrypted_otp_secret_iv: nil,
- encrypted_otp_secret_salt: nil,
- otp_backup_codes: nil
+ two_factor_enabled: false,
+ encrypted_otp_secret: nil,
+ encrypted_otp_secret_iv: nil,
+ encrypted_otp_secret_salt: nil,
+ otp_grace_period_started_at: nil,
+ otp_backup_codes: nil
)
end
def namespace_uniq
+ # Return early if username already failed the first uniqueness validation
+ return if self.errors.key?(:username) &&
+ self.errors[:username].include?('has already been taken')
+
namespace_name = self.username
existing_namespace = Namespace.by_path(namespace_name)
if existing_namespace && existing_namespace != self.namespace
- self.errors.add :username, "already exists"
+ self.errors.add(:username, 'has already been taken')
end
end
@@ -588,6 +624,13 @@ class User < ActiveRecord::Base
end
end
+ def try_obtain_ldap_lease
+ # After obtaining this lease LDAP checks will be blocked for 600 seconds
+ # (10 minutes) for this user.
+ lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
+ lease.try_obtain
+ end
+
def solo_owned_groups
@solo_owned_groups ||= owned_groups.select do |group|
group.owners == [self]
@@ -648,7 +691,10 @@ class User < ActiveRecord::Base
end
def all_emails
- [self.email, *self.emails.map(&:email)]
+ all_emails = []
+ all_emails << self.email unless self.temp_oauth_email?
+ all_emails.concat(self.emails.map(&:email))
+ all_emails
end
def hook_attrs
@@ -784,7 +830,8 @@ class User < ActiveRecord::Base
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
- projects.select(:id)])
+ projects.select(:id),
+ groups.joins(:shared_projects).select(:project_id)])
end
def ci_projects_union
@@ -800,4 +847,11 @@ class User < ActiveRecord::Base
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
+
+ def ensure_external_user_rights
+ return unless self.external?
+
+ self.can_create_group = false
+ self.projects_limit = 0
+ end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index e9413c34bae..526760779a4 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -62,7 +62,7 @@ class WikiPage
# The raw content of this page.
def content
@attributes[:content] ||= if @page
- @page.raw_data
+ @page.text_data
end
end
@@ -110,7 +110,7 @@ class WikiPage
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
- @page.historical?
+ @page.historical? && versions.first.sha != version.sha
end
# Returns boolean True or False if this instance
@@ -169,7 +169,7 @@ class WikiPage
private
def set_attributes
- attributes[:slug] = @page.escaped_url_path
+ attributes[:slug] = @page.url_path
attributes[:title] = @page.title
attributes[:format] = @page.format
end
diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb
deleted file mode 100644
index 2160bf13e6d..00000000000
--- a/app/services/archive_repository_service.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class ArchiveRepositoryService
- attr_reader :project, :ref, :format
-
- def initialize(project, ref, format)
- format ||= 'tar.gz'
- @project, @ref, @format = project, ref, format.downcase
- end
-
- def execute(options = {})
- RepositoryArchiveCacheWorker.perform_async
-
- metadata = project.repository.archive_metadata(ref, storage_path, format)
- raise "Repository or ref not found" if metadata.empty?
-
- metadata
- end
-
- private
-
- def storage_path
- Gitlab.config.gitlab.repository_downloads_path
- end
-end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index b48ca67d4d2..8563633816c 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -23,6 +23,10 @@ class BaseService
EventCreateService.new
end
+ def todo_service
+ TodoService.new
+ end
+
def log_info(message)
Gitlab::AppLogger.info message
end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index ad901f2da5d..002f7ba1278 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -34,6 +34,7 @@ module Ci
build = commit.builds.create!(build_attrs)
build.execute_hooks
+ build
end
end
end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index f469b13e902..50c95ced8a7 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,28 +1,23 @@
module Ci
class ImageForBuildService
- def execute(project, params)
- sha = params[:sha]
- sha ||=
- if params[:ref]
- project.commit(params[:ref]).try(:sha)
- end
+ def execute(project, opts)
+ sha = opts[:sha] || ref_sha(project, opts[:ref])
- commit = project.ci_commits.ordered.find_by(sha: sha)
+ commit = project.ci_commits.find_by(sha: sha)
image_name = image_for_commit(commit)
image_path = Rails.root.join('public/ci', image_name)
-
- OpenStruct.new(
- path: image_path,
- name: image_name
- )
+ OpenStruct.new(path: image_path, name: image_name)
end
private
+ def ref_sha(project, ref)
+ project.commit(ref).try(:sha) if ref
+ end
+
def image_for_commit(commit)
return 'build-unknown.svg' unless commit
-
'build-' + commit.status + ".svg"
end
end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
new file mode 100644
index 00000000000..a3c950ede1f
--- /dev/null
+++ b/app/services/commits/revert_service.rb
@@ -0,0 +1,59 @@
+module Commits
+ class RevertService < ::BaseService
+ class ValidationError < StandardError; end
+ class ReversionError < StandardError; end
+
+ def execute
+ @source_project = params[:source_project] || @project
+ @target_branch = params[:target_branch]
+ @commit = params[:commit]
+ @create_merge_request = params[:create_merge_request].present?
+
+ check_push_permissions unless @create_merge_request
+ commit
+ rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
+ ValidationError, ReversionError => ex
+ error(ex.message)
+ end
+
+ def commit
+ revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
+ revert_tree_id = repository.check_revert_content(@commit, @target_branch)
+
+ if revert_tree_id
+ create_target_branch(revert_into) if @create_merge_request
+
+ repository.revert(current_user, @commit, revert_into, revert_tree_id)
+ success
+ else
+ error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically.
+ It may have already been reverted, or a more recent commit may have updated some of its content."
+ raise ReversionError, error_msg
+ end
+ end
+
+ private
+
+ def create_target_branch(new_branch)
+ # Temporary branch exists and contains the revert commit
+ return success if repository.find_branch(new_branch)
+
+ result = CreateBranchService.new(@project, current_user)
+ .execute(new_branch, @target_branch, source_project: @source_project)
+
+ if result[:status] == :error
+ raise ReversionError, "There was an error creating the source branch: #{result[:message]}"
+ end
+ end
+
+ def check_push_permissions
+ allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
+
+ unless allowed
+ raise ValidationError.new('You are not allowed to push into this branch')
+ end
+
+ true
+ end
+ end
+end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index ec581658fc1..e2bccbdbcc3 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -1,7 +1,7 @@
require 'securerandom'
# Compare 2 branches for one repo or between repositories
-# and return Gitlab::CompareResult object that responds to commits and diffs
+# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
def execute(source_project, source_branch, target_project, target_branch, diff_options = {})
source_commit = source_project.commit(source_branch)
@@ -20,12 +20,10 @@ class CompareService
)
end
- Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- target_project.repository.raw_repository,
- target_branch,
- source_sha,
- ), diff_options
+ Gitlab::Git::Compare.new(
+ target_project.repository.raw_repository,
+ target_branch,
+ source_sha,
)
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index f139872c728..707c2f7ff85 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -29,12 +29,7 @@ class CreateBranchService < BaseService
end
if new_branch
- push_data = build_push_data(project, current_user, new_branch)
-
- EventCreateService.new.push(project, current_user, push_data)
- project.execute_hooks(push_data.dup, :push_hooks)
- project.execute_services(push_data.dup, :push_hooks)
-
+ # GitPushService handles execution of services and hooks for branch pushes
success(new_branch)
else
error('Invalid reference name')
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 31b407efeb1..69d5c42a877 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -33,7 +33,6 @@ class CreateCommitBuildsService
unless commit.skip_ci?
# Create builds for commit
tag = Gitlab::Git.tag_ref?(origin_ref)
- commit.update_committed!
commit.create_builds(ref, tag, user)
end
diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb
new file mode 100644
index 00000000000..59a66fde47a
--- /dev/null
+++ b/app/services/create_spam_log_service.rb
@@ -0,0 +1,13 @@
+class CreateSpamLogService < BaseService
+ def initialize(project, user, params)
+ super(project, user, params)
+ end
+
+ def execute
+ spam_params = params.merge({ user_id: @current_user.id,
+ project_id: @project.id } )
+ spam_log = SpamLog.new(spam_params)
+ spam_log.save
+ spam_log
+ end
+end
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index 2452999382a..55985380d31 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -23,6 +23,7 @@ class CreateTagService < BaseService
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
+ CreateCommitBuildsService.new.execute(project, current_user, push_data)
if release_description
CreateReleaseService.new(@project, @current_user).
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 22bf9dd935e..fae069ee4a5 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -25,12 +25,7 @@ class DeleteBranchService < BaseService
end
if repository.rm_branch(current_user, branch_name)
- push_data = build_push_data(branch)
-
- EventCreateService.new.push(project, current_user, push_data)
- project.execute_hooks(push_data.dup, :push_hooks)
- project.execute_services(push_data.dup, :push_hooks)
-
+ # GitPushService handles execution of services and hooks for branch pushes
success('Branch was removed')
else
error('Failed to remove branch')
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
index e622fd5ea5d..ce79287e35a 100644
--- a/app/services/delete_user_service.rb
+++ b/app/services/delete_user_service.rb
@@ -5,18 +5,22 @@ class DeleteUserService
@current_user = current_user
end
- def execute(user)
- if user.solo_owned_groups.present?
+ def execute(user, options = {})
+ if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
- user
- else
- user.personal_projects.each do |project|
- # Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
- end
+ return user
+ end
+
+ user.solo_owned_groups.each do |group|
+ DestroyGroupService.new(group, current_user).execute
+ end
- user.destroy
+ user.personal_projects.each do |project|
+ # Skip repository removal because we remove directory with namespace
+ # that contain all this repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
end
+
+ user.destroy
end
end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index d929a676293..3c42ac61be4 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -6,12 +6,12 @@ class DestroyGroupService
end
def execute
- @group.projects.each do |project|
+ group.projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
end
- @group.destroy
+ group.destroy
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index d7ea30bc315..14e2a2c0699 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -1,10 +1,10 @@
-class GitPushService
- attr_accessor :project, :user, :push_data, :push_commits
+class GitPushService < BaseService
+ attr_accessor :push_data, :push_commits
include Gitlab::CurrentSettings
include Gitlab::Access
# This method will be called after each git update
- # and only if the provided user and project is present in GitLab.
+ # and only if the provided user and project are present in GitLab.
#
# All callbacks for post receive action should be placed here.
#
@@ -12,65 +12,93 @@ class GitPushService
# 1. Creates the push event
# 2. Updates merge requests
# 3. Recognizes cross-references from commit messages
- # 4. Executes the project's web hooks
+ # 4. Executes the project's webhooks
# 5. Executes the project's services
+ # 6. Checks if the project's main language has changed
#
- def execute(project, user, oldrev, newrev, ref)
- @project, @user = project, user
+ def execute
+ @project.repository.after_push_commit(branch_name, params[:newrev])
- project.repository.expire_cache
-
- if push_remove_branch?(ref, newrev)
+ if push_remove_branch?
+ @project.repository.after_remove_branch
@push_commits = []
- elsif push_to_new_branch?(ref, oldrev)
+ elsif push_to_new_branch?
+ @project.repository.after_create_branch
+
# Re-find the pushed commits.
- if is_default_branch?(ref)
+ if is_default_branch?
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
- @push_commits = project.repository.commits(newrev)
-
- # Ensure HEAD points to the default branch in case it is not master
- branch_name = Gitlab::Git.ref_name(ref)
- project.change_head(branch_name)
-
- # Set protection on the default branch if configured
- if (current_application_settings.default_branch_protection != PROTECTION_NONE)
- developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
- project.protected_branches.create({ name: project.default_branch, developers_can_push: developers_can_push })
- end
+ process_default_branch
else
# Use the pushed commits that aren't reachable by the default branch
# as a heuristic. This may include more commits than are actually pushed, but
# that shouldn't matter because we check for existing cross-references later.
- @push_commits = project.repository.commits_between(project.default_branch, newrev)
+ @push_commits = @project.repository.commits_between(@project.default_branch, params[:newrev])
# don't process commits for the initial push to the default branch
- process_commit_messages(ref)
+ process_commit_messages
end
- elsif push_to_existing_branch?(ref, oldrev)
+ elsif push_to_existing_branch?
# Collect data for this git push
- @push_commits = project.repository.commits_between(oldrev, newrev)
- process_commit_messages(ref)
+ @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
+ process_commit_messages
end
-
+ # Checks if the main language has changed in the project and if so
+ # it updates it accordingly
+ update_main_language
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
- project.update_merge_requests(oldrev, newrev, ref, @user)
+ update_merge_requests
- @push_data = build_push_data(oldrev, newrev, ref)
+ perform_housekeeping
+ end
+
+ def update_main_language
+ current_language = @project.repository.main_language
- EventCreateService.new.push(project, user, @push_data)
- project.execute_hooks(@push_data.dup, :push_hooks)
- project.execute_services(@push_data.dup, :push_hooks)
- CreateCommitBuildsService.new.execute(project, @user, @push_data)
- ProjectCacheWorker.perform_async(project.id)
+ unless current_language == @project.main_language
+ return @project.update_attributes(main_language: current_language)
+ end
+
+ true
end
protected
+ def update_merge_requests
+ @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user)
+
+ EventCreateService.new.push(@project, current_user, build_push_data)
+ @project.execute_hooks(build_push_data.dup, :push_hooks)
+ @project.execute_services(build_push_data.dup, :push_hooks)
+ CreateCommitBuildsService.new.execute(@project, current_user, build_push_data)
+ ProjectCacheWorker.perform_async(@project.id)
+ end
+
+ def perform_housekeeping
+ housekeeping = Projects::HousekeepingService.new(@project)
+ housekeeping.increment!
+ housekeeping.execute if housekeeping.needed?
+ rescue Projects::HousekeepingService::LeaseTaken
+ end
+
+ def process_default_branch
+ @push_commits = project.repository.commits(params[:newrev])
+
+ # Ensure HEAD points to the default branch in case it is not master
+ project.change_head(branch_name)
+
+ # Set protection on the default branch if configured
+ if current_application_settings.default_branch_protection != PROTECTION_NONE
+ developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
+ @project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
+ end
+ end
+
# Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
# close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
- def process_commit_messages(ref)
- is_default_branch = is_default_branch?(ref)
+ def process_commit_messages
+ is_default_branch = is_default_branch?
authors = Hash.new do |hash, commit|
email = commit.author_email
@@ -89,9 +117,11 @@ class GitPushService
# Close issues if these commits were pushed to the project's default branch and the commit message matches the
# closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to
# a different branch.
- closed_issues = commit.closes_issues(user)
+ closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
- Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
+ if can?(current_user, :update_issue, issue)
+ Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
+ end
end
end
@@ -99,34 +129,38 @@ class GitPushService
end
end
- def build_push_data(oldrev, newrev, ref)
- Gitlab::PushDataBuilder.
- build(project, user, oldrev, newrev, ref, push_commits)
+ def build_push_data
+ @push_data ||= Gitlab::PushDataBuilder.
+ build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
end
- def push_to_existing_branch?(ref, oldrev)
+ def push_to_existing_branch?
# Return if this is not a push to a branch (e.g. new commits)
- Gitlab::Git.branch_ref?(ref) && !Gitlab::Git.blank_ref?(oldrev)
+ Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev])
end
- def push_to_new_branch?(ref, oldrev)
- Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(oldrev)
+ def push_to_new_branch?
+ Gitlab::Git.branch_ref?(params[:ref]) && Gitlab::Git.blank_ref?(params[:oldrev])
end
- def push_remove_branch?(ref, newrev)
- Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(newrev)
+ def push_remove_branch?
+ Gitlab::Git.branch_ref?(params[:ref]) && Gitlab::Git.blank_ref?(params[:newrev])
end
- def push_to_branch?(ref)
- Gitlab::Git.branch_ref?(ref)
+ def push_to_branch?
+ Gitlab::Git.branch_ref?(params[:ref])
end
- def is_default_branch?(ref)
- Gitlab::Git.branch_ref?(ref) &&
- (Gitlab::Git.ref_name(ref) == project.default_branch || project.default_branch.nil?)
+ def is_default_branch?
+ Gitlab::Git.branch_ref?(params[:ref]) &&
+ (Gitlab::Git.ref_name(params[:ref]) == project.default_branch || project.default_branch.nil?)
end
def commit_user(commit)
- commit.author || user
+ commit.author || current_user
+ end
+
+ def branch_name
+ @branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
end
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 4144c7111d0..c88c7672805 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref)
- project.repository.expire_cache
+ project.repository.before_push_tag
@project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref)
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2556f06e2d3..18f76d3f650 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -11,7 +11,10 @@ class IssuableBaseService < BaseService
issuable, issuable.project, current_user, issuable.milestone)
end
- def create_labels_note(issuable, added_labels, removed_labels)
+ def create_labels_note(issuable, old_labels)
+ added_labels = issuable.labels - old_labels
+ removed_labels = old_labels - issuable.labels
+
SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels)
end
@@ -54,7 +57,7 @@ class IssuableBaseService < BaseService
if params.present? && issuable.update_attributes(params.merge(updated_by: current_user))
issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
- handle_changes(issuable)
+ handle_changes(issuable, old_labels: old_labels)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
@@ -71,7 +74,19 @@ class IssuableBaseService < BaseService
end
end
- def handle_common_system_notes(issuable, options = {})
+ def has_changes?(issuable, old_labels: [])
+ valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
+
+ attrs_changed = valid_attrs.any? do |attr|
+ issuable.previous_changes.include?(attr.to_s)
+ end
+
+ labels_changed = issuable.labels != old_labels
+
+ attrs_changed || labels_changed
+ end
+
+ def handle_common_system_notes(issuable, old_labels: [])
if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first)
end
@@ -80,9 +95,6 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable)
end
- old_labels = options[:old_labels]
- if old_labels && (issuable.labels != old_labels)
- create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
- end
+ create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index a1a20e47681..78254b49af3 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -3,6 +3,7 @@ module Issues
def execute(issue, commit = nil)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
+ todo_service.close_issue(issue, current_user)
return issue
end
@@ -10,6 +11,7 @@ module Issues
event_service.close_issue(issue, current_user)
create_note(issue, commit)
notification_service.close_issue(issue, current_user)
+ 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 bcb380d3215..10787e8873c 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -9,6 +9,7 @@ module Issues
if issue.save
issue.update_attributes(label_ids: label_params)
notification_service.new_issue(issue, current_user)
+ todo_service.new_issue(issue, current_user)
event_service.open_issue(issue, current_user)
issue.create_cross_references!(current_user)
execute_hooks(issue, 'open')
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a55a04dd5e0..3563cbaa997 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -4,7 +4,16 @@ module Issues
update(issue)
end
- def handle_changes(issue)
+ def handle_changes(issue, old_labels: [])
+ if has_changes?(issue, old_labels: old_labels)
+ todo_service.mark_pending_todos_as_done(issue, current_user)
+ end
+
+ if issue.previous_changes.include?('title') ||
+ issue.previous_changes.include?('description')
+ todo_service.update_issue(issue, current_user)
+ end
+
if issue.previous_changes.include?('milestone_id')
create_milestone_note(issue)
end
@@ -12,6 +21,12 @@ module Issues
if issue.previous_changes.include?('assignee_id')
create_assignee_note(issue)
notification_service.reassigned_issue(issue, current_user)
+ todo_service.reassigned_issue(issue, current_user)
+ end
+
+ added_labels = issue.labels - old_labels
+ if added_labels.present?
+ notification_service.relabeled_issue(issue, added_labels, current_user)
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index a9b29f9654d..fa34753c4fd 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -5,9 +5,7 @@ module MergeRequests
# Set MR attributes
merge_request.can_be_created = false
- merge_request.compare_failed = false
merge_request.compare_commits = []
- merge_request.compare_diffs = []
merge_request.source_project = project unless merge_request.source_project
merge_request.target_project ||= (project.forked_from_project || project)
merge_request.target_branch ||= merge_request.target_project.default_branch
@@ -21,46 +19,49 @@ module MergeRequests
return build_failed(merge_request, message)
end
- compare_result = CompareService.new.execute(
+ compare = CompareService.new.execute(
merge_request.source_project,
merge_request.source_branch,
merge_request.target_project,
merge_request.target_branch,
)
- commits = compare_result.commits
+ commits = compare.commits
# At this point we decide if merge request can be created
# If we have at least one commit to merge -> creation allowed
if commits.present?
merge_request.compare_commits = Commit.decorate(commits, merge_request.source_project)
merge_request.can_be_created = true
- merge_request.compare_failed = false
-
- # Try to collect diff for merge request.
- diffs = compare_result.diffs
-
- if diffs.present?
- merge_request.compare_diffs = diffs
-
- elsif diffs == false
- merge_request.can_be_created = false
- merge_request.compare_failed = true
- end
+ merge_request.compare = compare
else
merge_request.can_be_created = false
- merge_request.compare_failed = false
end
commits = merge_request.compare_commits
if commits && commits.count == 1
commit = commits.first
merge_request.title = commit.title
- merge_request.description = commit.description.try(:strip)
+ merge_request.description ||= commit.description.try(:strip)
else
merge_request.title = merge_request.source_branch.titleize.humanize
end
+ # When your branch name starts with an iid followed by a dash this pattern will
+ # 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+)-/)
+ iid = match[1]
+ closes_issue = "Closes ##{iid}"
+
+ if merge_request.description.present?
+ merge_request.description << closes_issue.prepend("\n")
+ else
+ merge_request.description = closes_issue
+ end
+ end
+
merge_request
end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index 47454f9f0c2..27ee81fe3e7 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -9,6 +9,7 @@ module MergeRequests
event_service.close_mr(merge_request, current_user)
create_note(merge_request)
notification_service.close_mr(merge_request, current_user)
+ todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 009d5a6867e..33609d01f20 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -2,7 +2,7 @@ module MergeRequests
class CreateService < MergeRequests::BaseService
def execute
# @project is used to determine whether the user can set the merge request's
- # assignee, milestone and labels. Whether they can depends on their
+ # assignee, milestone and labels. Whether they can depends on their
# permissions on the target project.
source_project = @project
@project = Project.find(params[:target_project_id]) if params[:target_project_id]
@@ -18,6 +18,7 @@ module MergeRequests
merge_request.update_attributes(label_ids: label_params)
event_service.open_mr(merge_request, current_user)
notification_service.new_merge_request(merge_request, current_user)
+ todo_service.new_merge_request(merge_request, current_user)
merge_request.create_cross_references!(current_user)
execute_hooks(merge_request)
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index cabc3d8fabb..9a58383b398 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -34,7 +34,8 @@ module MergeRequests
committer: committer
}
- repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
+ commit_id = repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
+ merge_request.update(merge_commit_sha: commit_id)
rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge")
Rails.logger.error(e.message)
@@ -44,7 +45,7 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
- if params[:should_remove_source_branch]
+ if params[:should_remove_source_branch].present?
DeleteBranchService.new(@merge_request.source_project, current_user).
execute(merge_request.source_branch)
end
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
index 5cf7404a493..d6af12f9739 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -19,15 +19,19 @@ module MergeRequests
end
# Triggers the automatic merge of merge_request once the build succeeds
- def trigger(build)
- merge_requests = merge_request_from(build)
+ def trigger(commit_status)
+ merge_requests = merge_request_from(commit_status)
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds?
+ next unless merge_request.mergeable?
- if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable?
- MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
- end
+ ci_commit = merge_request.ci_commit
+ next unless ci_commit
+ next unless ci_commit.sha == commit_status.sha
+ next unless ci_commit.success?
+
+ MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
end
@@ -45,9 +49,16 @@ module MergeRequests
private
- def merge_request_from(build)
- merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a
- merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a
+ def merge_request_from(commit_status)
+ branches = commit_status.ref
+
+ # This is for ref-less builds
+ branches ||= @project.repository.branch_names_contains(commit_status.sha)
+
+ return [] if branches.blank?
+
+ merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
+ merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 8f25c5e2496..ebb67c7db65 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -21,7 +21,9 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue|
- Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ if can?(current_user, :update_issue, issue)
+ Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 5ff2cc03dda..477c64e7377 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -14,7 +14,16 @@ module MergeRequests
update(merge_request)
end
- def handle_changes(merge_request)
+ def handle_changes(merge_request, old_labels: [])
+ if has_changes?(merge_request, old_labels: old_labels)
+ todo_service.mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ if merge_request.previous_changes.include?('title') ||
+ merge_request.previous_changes.include?('description')
+ todo_service.update_merge_request(merge_request, current_user)
+ end
+
if merge_request.previous_changes.include?('target_branch')
create_branch_change_note(merge_request, 'target',
merge_request.previous_changes['target_branch'].first,
@@ -28,12 +37,22 @@ module MergeRequests
if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user)
+ todo_service.reassigned_merge_request(merge_request, current_user)
end
if merge_request.previous_changes.include?('target_branch') ||
merge_request.previous_changes.include?('source_branch')
merge_request.mark_as_unchecked
end
+
+ added_labels = merge_request.labels - old_labels
+ if added_labels.present?
+ notification_service.relabeled_merge_request(
+ merge_request,
+ added_labels,
+ current_user
+ )
+ end
end
def reopen_service
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index a8486e6a5a1..2bb312bb252 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -6,27 +6,12 @@ module Notes
note.system = false
if note.save
- notification_service.new_note(note)
-
- # Skip system notes, like status changes and cross-references and awards
- unless note.system || note.is_award
- event_service.leave_note(note, note.author)
- note.create_cross_references!
- execute_hooks(note)
- end
+ # Finish the harder work in the background
+ NewNoteWorker.perform_in(2.seconds, note.id, params)
+ TodoService.new.new_note(note, current_user)
end
note
end
-
- def hook_data(note)
- Gitlab::NoteDataBuilder.build(note, current_user)
- end
-
- def execute_hooks(note)
- note_data = hook_data(note)
- note.project.execute_hooks(note_data, :note_hooks)
- note.project.execute_services(note_data, :note_hooks)
- end
end
end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
new file mode 100644
index 00000000000..e818f58d13c
--- /dev/null
+++ b/app/services/notes/post_process_service.rb
@@ -0,0 +1,28 @@
+module Notes
+ class PostProcessService
+ attr_accessor :note
+
+ def initialize(note)
+ @note = note
+ end
+
+ def execute
+ # Skip system notes, like status changes and cross-references and awards
+ unless @note.system || @note.is_award
+ EventCreateService.new.leave_note(@note, @note.author)
+ @note.create_cross_references!
+ execute_note_hooks
+ end
+ end
+
+ def hook_data
+ Gitlab::NoteDataBuilder.build(@note, @note.author)
+ end
+
+ def execute_note_hooks
+ note_data = hook_data
+ @note.project.execute_hooks(note_data, :note_hooks)
+ @note.project.execute_services(note_data, :note_hooks)
+ end
+ end
+end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 72e2f78008d..1361b1e0300 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -7,6 +7,10 @@ module Notes
note.create_new_cross_references!(current_user)
note.reset_events_cache
+ if note.previous_changes.include?('note')
+ TodoService.new.update_note(note, current_user)
+ end
+
note
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index bdf7b3ad2bb..19a6779dea9 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -24,16 +24,17 @@ class NotificationService
end
end
- # When create an issue we should send next emails:
+ # When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
+ # * watchers of the issue's labels
#
def new_issue(issue, current_user)
new_resource_email(issue, issue.project, 'new_issue_email')
end
- # When we close an issue we should send next emails:
+ # When we close an issue we should send an email to:
#
# * issue author if their notification level is not Disabled
# * issue assignee if their notification level is not Disabled
@@ -43,7 +44,7 @@ class NotificationService
close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
end
- # When we reassign an issue we should send next emails:
+ # When we reassign an issue we should send an email to:
#
# * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled
@@ -52,16 +53,25 @@ class NotificationService
reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
end
+ # When we add labels to an issue we should send an email to:
+ #
+ # * watchers of the issue's labels
+ #
+ def relabeled_issue(issue, added_labels, current_user)
+ relabeled_resource_email(issue, added_labels, current_user, 'relabeled_issue_email')
+ end
- # When create a merge request we should send next emails:
+ # When create a merge request we should send an email to:
#
# * mr assignee if their notification level is not Disabled
+ # * project team members with notification level higher then Participating
+ # * watchers of the mr's labels
#
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
end
- # When we reassign a merge_request we should send next emails:
+ # When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled
@@ -70,6 +80,14 @@ class NotificationService
reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
end
+ # When we add labels to a merge request we should send an email to:
+ #
+ # * watchers of the mr's labels
+ #
+ def relabeled_merge_request(merge_request, added_labels, current_user)
+ relabeled_resource_email(merge_request, added_labels, current_user, 'relabeled_merge_request_email')
+ end
+
def close_mr(merge_request, current_user)
close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email')
end
@@ -91,7 +109,8 @@ class NotificationService
reopen_resource_email(
merge_request,
merge_request.target_project,
- current_user, 'merge_request_status_email',
+ current_user,
+ 'merge_request_status_email',
'reopened'
)
end
@@ -348,19 +367,23 @@ class NotificationService
end
def add_subscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
+ return recipients unless target.respond_to? :subscribers
+
+ recipients + target.subscribers
+ end
- subscriptions = target.subscriptions
+ def add_labels_subscribers(recipients, target, labels: nil)
+ return recipients unless target.respond_to? :labels
- if subscriptions.any?
- recipients + subscriptions.where(subscribed: true).map(&:user)
- else
- recipients
+ (labels || target.labels).each do |label|
+ recipients += label.subscribers
end
+
+ recipients
end
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author)
+ recipients = build_recipients(target, project, target.author, action: :new)
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -376,10 +399,10 @@ class NotificationService
end
def reassign_resource_email(target, project, current_user, method)
- previous_assignee_id = previous_record(target, "assignee_id")
+ previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = build_recipients(target, project, current_user, [previous_assignee])
+ recipients = build_recipients(target, project, current_user, action: :reassign, previous_assignee: previous_assignee)
recipients.each do |recipient|
mailer.send(
@@ -392,6 +415,15 @@ class NotificationService
end
end
+ def relabeled_resource_email(target, labels, current_user, method)
+ recipients = build_relabeled_recipients(target, current_user, labels: labels)
+ label_names = labels.map(&:name)
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.id, target.id, label_names, current_user.id).deliver_later
+ end
+ end
+
def reopen_resource_email(target, project, current_user, method, status)
recipients = build_recipients(target, project, current_user)
@@ -400,21 +432,39 @@ class NotificationService
end
end
- def build_recipients(target, project, current_user, extra_recipients = nil)
+ def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
recipients = target.participants(current_user)
- recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients
-
recipients = add_project_watchers(recipients, project)
recipients = reject_mention_users(recipients, project)
- recipients = reject_muted_users(recipients, project)
+ # Re-assign is considered as a mention of the new assignee so we add the
+ # new assignee to the list of recipients after we rejected users with
+ # the "on mention" notification level
+ if action == :reassign
+ recipients << previous_assignee if previous_assignee
+ recipients << target.assignee
+ end
+
+ recipients = reject_muted_users(recipients, project)
recipients = add_subscribed_users(recipients, target)
+
+ if action == :new
+ recipients = add_labels_subscribers(recipients, target)
+ end
+
recipients = reject_unsubscribed_users(recipients, target)
recipients.delete(current_user)
- recipients
+ 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.delete(current_user)
+ recipients.uniq
end
def mailer
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/destroy_service.rb b/app/services/projects/destroy_service.rb
index 28872c89259..df5054f08d7 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -6,15 +6,25 @@ module Projects
DELETED_FLAG = '+deleted'
+ def pending_delete!
+ project.update_attribute(:pending_delete, true)
+
+ ProjectDestroyWorker.perform_in(1.minute, project.id, current_user.id, params)
+ end
+
def execute
return false unless can?(current_user, :remove_project, project)
project.team.truncate
- project.repository.expire_cache unless project.empty_repo?
repo_path = project.path_with_namespace
wiki_path = repo_path + '.wiki'
+ # Flush the cache for both repositories. This has to be done _before_
+ # removing the physical repositories as some expiration code depends on
+ # Git data (e.g. a list of branch names).
+ flush_caches(project, wiki_path)
+
Project.transaction do
project.destroy!
@@ -64,5 +74,11 @@ module Projects
def removal_path(path)
"#{path}+#{project.id}#{DELETED_FLAG}"
end
+
+ def flush_caches(project, wiki_path)
+ project.repository.before_delete
+
+ Repository.new(wiki_path, project).before_delete
+ end
end
end
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index 99f22293d0d..6386f57fb0d 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -16,13 +16,7 @@ module Projects
uploader.download!(@url)
uploader.store!
- filename = uploader.image? ? uploader.file.basename : uploader.file.filename
-
- {
- 'alt' => filename,
- 'url' => uploader.secure_url,
- 'is_image' => uploader.image?
- }
+ uploader.to_h
end
private
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
new file mode 100644
index 00000000000..bccd67d3dbf
--- /dev/null
+++ b/app/services/projects/housekeeping_service.rb
@@ -0,0 +1,47 @@
+# Projects::HousekeepingService class
+#
+# Used for git housekeeping
+#
+# Ex.
+# Projects::HousekeepingService.new(project).execute
+#
+module Projects
+ class HousekeepingService < BaseService
+ include Gitlab::ShellAdapter
+
+ LEASE_TIMEOUT = 3600
+
+ class LeaseTaken < StandardError
+ def to_s
+ "Somebody already triggered housekeeping for this project in the past #{LEASE_TIMEOUT / 60} minutes"
+ end
+ end
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ raise LeaseTaken if !try_obtain_lease
+
+ GitlabShellWorker.perform_async(:gc, @project.path_with_namespace)
+ ensure
+ @project.update_column(:pushes_since_gc, 0)
+ end
+
+ def needed?
+ @project.pushes_since_gc >= 10
+ end
+
+ def increment!
+ @project.increment!(:pushes_since_gc)
+ end
+
+ private
+
+ def try_obtain_lease
+ lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
+ lease.try_obtain
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
new file mode 100644
index 00000000000..2015897dd19
--- /dev/null
+++ b/app/services/projects/import_service.rb
@@ -0,0 +1,67 @@
+module Projects
+ class ImportService < BaseService
+ include Gitlab::ShellAdapter
+
+ class Error < StandardError; end
+
+ ALLOWED_TYPES = [
+ 'bitbucket',
+ 'fogbugz',
+ 'gitlab',
+ 'github',
+ 'google_code'
+ ]
+
+ def execute
+ if unknown_url?
+ # In this case, we only want to import issues, not a repository.
+ create_repository
+ else
+ import_repository
+ end
+
+ import_data
+
+ success
+ rescue Error => e
+ error(e.message)
+ end
+
+ private
+
+ def create_repository
+ unless project.create_repository
+ raise Error, 'The repository could not be created.'
+ end
+ end
+
+ def import_repository
+ begin
+ gitlab_shell.import_repository(project.path_with_namespace, project.import_url)
+ rescue Gitlab::Shell::Error => e
+ raise Error, e.message
+ end
+ end
+
+ def import_data
+ return unless has_importer?
+
+ unless importer.execute
+ raise Error, 'The remote data could not be imported.'
+ end
+ end
+
+ def has_importer?
+ ALLOWED_TYPES.include?(project.import_type)
+ end
+
+ def importer
+ class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
+ class_name.constantize.new(project)
+ end
+
+ def unknown_url?
+ project.import_url == Project::UNKNOWN_IMPORT_URL
+ end
+ end
+end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 64ea6dd42eb..2e734654466 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -55,6 +55,9 @@ module Projects
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ project.old_path_with_namespace = old_path
+
+ SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end
end
diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb
index 279550d6f4a..012e82a7704 100644
--- a/app/services/projects/upload_service.rb
+++ b/app/services/projects/upload_service.rb
@@ -10,13 +10,7 @@ module Projects
uploader = FileUploader.new(@project)
uploader.store!(@file)
- filename = uploader.image? ? uploader.file.basename : uploader.file.filename
-
- {
- alt: filename,
- url: uploader.secure_url,
- is_image: uploader.image?
- }
+ uploader.to_h
end
private
diff --git a/app/services/repair_ldap_blocked_user_service.rb b/app/services/repair_ldap_blocked_user_service.rb
new file mode 100644
index 00000000000..863cef7ff61
--- /dev/null
+++ b/app/services/repair_ldap_blocked_user_service.rb
@@ -0,0 +1,17 @@
+class RepairLdapBlockedUserService
+ attr_accessor :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ def execute
+ user.block if ldap_hard_blocked?
+ end
+
+ private
+
+ def ldap_hard_blocked?
+ user.ldap_blocked? && !user.ldap_user?
+ end
+end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index e904cb6c6fc..aa9837038a6 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -10,9 +10,8 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
- project_ids = projects.pluck(:id)
- Gitlab::SearchResults.new(project_ids, 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 f630c0a3790..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.id,
+ Gitlab::ProjectSearchResults.new(current_user,
+ project,
params[:search],
params[:repository_ref])
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 8ca0877321d..0b3e713e220 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -7,8 +7,9 @@ module Search
end
def execute
- snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
- Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
+ snippets = Snippet.accessible_to(current_user)
+
+ Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 8b5143e1eb7..ea2b26ccb52 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -18,7 +18,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
- created_at: model.created_at.xmlschema
+ created_at: model.created_at.xmlschema,
+ updated_at: model.updated_at.xmlschema
}
case model
@@ -34,11 +35,20 @@ class SystemHooksService
end
when Project
data.merge!(project_data(model))
+
+ if event == :rename || event == :transfer
+ data.merge!({
+ old_path_with_namespace: model.old_path_with_namespace
+ })
+ end
+
+ data
when User
data.merge!({
name: model.name,
email: model.email,
- user_id: model.id
+ user_id: model.id,
+ username: model.username
})
when ProjectMember
data.merge!(project_member_data(model))
@@ -90,8 +100,10 @@ class SystemHooksService
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
}
@@ -102,6 +114,7 @@ class SystemHooksService
group_name: model.group.name,
group_path: model.group.path,
group_id: model.group.id,
+ user_username: model.user.username,
user_name: model.user.name,
user_email: model.user.email,
user_id: model.user.id,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 98a71cbf1ad..f09b77c4a57 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -41,7 +41,7 @@ class SystemNoteService
#
# Returns the created Note object
def self.change_assignee(noteable, project, author, assignee)
- body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}"
+ body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -66,7 +66,7 @@ class SystemNoteService
def self.change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
- references = ->(label) { "~#{label.id}" }
+ references = ->(label) { label.to_reference(format: :id) }
added_labels = added_labels.map(&references).join(' ')
removed_labels = removed_labels.map(&references).join(' ')
@@ -103,7 +103,7 @@ class SystemNoteService
# Returns the created Note object
def self.change_milestone(noteable, project, author, milestone)
body = 'Milestone '
- body += milestone.nil? ? 'removed' : "changed to #{milestone.title}"
+ body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -207,6 +207,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when a branch is created from the 'new branch' button on a issue
+ # Example note text:
+ #
+ # "Started branch `201-issue-branch-button`"
+ def self.new_issue_branch(issue, project, author, branch)
+ h = Gitlab::Application.routes.url_helpers
+ link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
+
+ body = "Started branch [`#{branch}`](#{link})"
+ create_note(noteable: issue, project: project, author: author, note: body)
+ end
+
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
@@ -274,12 +286,15 @@ class SystemNoteService
# Check if a cross reference to a noteable from a mentioner already exists
#
# This method is used to prevent multiple notes being created for a mention
- # when a issue is updated, for example.
+ # when a issue is updated, for example. The method also calls notes_for_mentioner
+ # to check if the mentioner is a commit, and return matches only on commit hash
+ # instead of project + commit, to avoid repeated mentions from forks.
#
# noteable - Noteable object being referenced
# mentioner - Mentionable object
#
# Returns Boolean
+
def self.cross_reference_exists?(noteable, mentioner)
# Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class)
@@ -291,14 +306,20 @@ class SystemNoteService
notes = notes.where(noteable_id: noteable.id)
end
- gfm_reference = mentioner.gfm_reference(noteable.project)
- notes = notes.where(note: cross_reference_note_content(gfm_reference))
-
- notes.count > 0
+ notes_for_mentioner(mentioner, noteable, notes).count > 0
end
private
+ def self.notes_for_mentioner(mentioner, noteable, notes)
+ if mentioner.is_a?(Commit)
+ notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}")
+ else
+ gfm_reference = mentioner.gfm_reference(noteable.project)
+ notes.where(note: cross_reference_note_content(gfm_reference))
+ end
+ end
+
def self.create_note(args = {})
Note.create(args.merge(system: true))
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
new file mode 100644
index 00000000000..4392e2d17fe
--- /dev/null
+++ b/app/services/todo_service.rb
@@ -0,0 +1,170 @@
+# TodoService class
+#
+# Used for creating todos after certain user actions
+#
+# Ex.
+# TodoService.new.new_issue(issue, current_user)
+#
+class TodoService
+ # When create an issue we should:
+ #
+ # * create a todo for assignee if issue is assigned
+ # * create a todo for each mentioned user on issue
+ #
+ def new_issue(issue, current_user)
+ new_issuable(issue, current_user)
+ end
+
+ # When update an issue we should:
+ #
+ # * mark all pending todos related to the issue for the current user as done
+ #
+ def update_issue(issue, current_user)
+ create_mention_todos(issue.project, issue, current_user)
+ end
+
+ # When close an issue we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def close_issue(issue, current_user)
+ mark_pending_todos_as_done(issue, current_user)
+ end
+
+ # When we reassign an issue we should:
+ #
+ # * create a pending todo for new assignee if issue is assigned
+ #
+ def reassigned_issue(issue, current_user)
+ create_assignment_todo(issue, current_user)
+ end
+
+ # When create a merge request we should:
+ #
+ # * creates a pending todo for assignee if merge request is assigned
+ # * create a todo for each mentioned user on merge request
+ #
+ def new_merge_request(merge_request, current_user)
+ new_issuable(merge_request, current_user)
+ end
+
+ # When update a merge request we should:
+ #
+ # * create a todo for each mentioned user on merge request
+ #
+ def update_merge_request(merge_request, current_user)
+ create_mention_todos(merge_request.project, merge_request, current_user)
+ end
+
+ # When close a merge request we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def close_merge_request(merge_request, current_user)
+ mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ # When we reassign a merge request we should:
+ #
+ # * creates a pending todo for new assignee if merge request is assigned
+ #
+ def reassigned_merge_request(merge_request, current_user)
+ create_assignment_todo(merge_request, current_user)
+ end
+
+ # When merge a merge request we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def merge_merge_request(merge_request, current_user)
+ mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ # When create a note we should:
+ #
+ # * mark all pending todos related to the noteable for the note author as done
+ # * create a todo for each mentioned user on note
+ #
+ def new_note(note, current_user)
+ handle_note(note, current_user)
+ end
+
+ # When update a note we should:
+ #
+ # * mark all pending todos related to the noteable for the current user as done
+ # * create a todo for each new user mentioned on note
+ #
+ def update_note(note, current_user)
+ handle_note(note, current_user)
+ end
+
+ # When marking pending todos as done we should:
+ #
+ # * 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)
+ end
+
+ private
+
+ def create_todos(project, target, author, users, action, note = nil)
+ 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
+ )
+ end
+ end
+
+ def new_issuable(issuable, author)
+ create_assignment_todo(issuable, author)
+ create_mention_todos(issuable.project, issuable, author)
+ 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)
+
+ project = note.project
+ target = note.noteable
+
+ mark_pending_todos_as_done(target, author)
+ create_mention_todos(project, target, author, note)
+ end
+
+ def create_assignment_todo(issuable, author)
+ if issuable.assignee && issuable.assignee != author
+ create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED)
+ 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)
+ end
+
+ def filter_mentioned_users(project, target, author)
+ mentioned_users = target.mentioned_users.select do |user|
+ user.can?(:read_project, project)
+ end
+
+ mentioned_users.delete(author)
+ mentioned_users.uniq
+ end
+
+ def pending_todos(user, project, target)
+ user.todos.pending.where(
+ project_id: project.id,
+ target_id: target.id,
+ target_type: target.class.name
+ )
+ end
+end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 1b0ae6c0056..1cd93263c9f 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -32,6 +32,10 @@ class ArtifactUploader < CarrierWave::Uploader::Base
self.class.storage == CarrierWave::Storage::File
end
+ def filename
+ file.try(:filename)
+ end
+
def exists?
file.try(:exists?)
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index ac920119a85..86d24469e05 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -30,4 +30,19 @@ class FileUploader < CarrierWave::Uploader::Base
def secure_url
File.join("/uploads", @secret, file.filename)
end
+
+ def to_h
+ filename = image? ? self.file.basename : self.file.filename
+ escaped_filename = filename.gsub("]", "\\]")
+
+ markdown = "[#{escaped_filename}](#{self.secure_url})"
+ markdown.prepend("!") if image?
+
+ {
+ alt: filename,
+ url: self.secure_url,
+ is_image: image?,
+ markdown: markdown
+ }
+ end
end
diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb
index b35af100803..aab07a7ece4 100644
--- a/app/validators/email_validator.rb
+++ b/app/validators/email_validator.rb
@@ -1,18 +1,5 @@
-# EmailValidator
-#
-# Based on https://github.com/balexand/email_validator
-#
-# Extended to use only strict mode with following allowed characters:
-# ' - apostrophe
-#
-# See http://www.remote.org/jochen/mail/info/chars.html
-#
class EmailValidator < ActiveModel::EachValidator
- PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze
-
def validate_each(record, attribute, value)
- unless value =~ PATTERN
- record.errors.add(attribute, options[:message] || :invalid)
- end
+ record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp
end
end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 10e35ce665a..7a35958cc5f 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -17,6 +17,7 @@ class NamespaceValidator < ActiveModel::EachValidator
hooks
issues
merge_requests
+ new
notes
profile
projects
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index 2848b9cd33d..a77beb2683d 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -29,8 +29,11 @@ class UrlValidator < ActiveModel::EachValidator
end
def valid_url?(value)
+ return false if value.nil?
+
options = default_options.merge(self.options)
+ value.strip!
value =~ /\A#{URI.regexp(options[:protocols])}\z/
end
end
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
index 619533e09a7..2741eb44357 100644
--- a/app/views/abuse_report_mailer/notify.html.haml
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -8,4 +8,4 @@
= @abuse_report.message
%p
- = link_to "View details", abuse_reports_url
+ = link_to "View details", admin_abuse_reports_url
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index cffd7684008..3bc1b24b5e2 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -2,7 +2,7 @@
%h3.page-title Report abuse
%p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr
-= form_for @abuse_report, html: { class: 'form-horizontal'} do |f|
+= form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f|
= f.hidden_field :user_id
- if @abuse_report.errors.any?
.alert.alert-danger
@@ -16,7 +16,7 @@
.form-group
= f.label :message, class: 'control-label'
.col-sm-10
- = f.text_area :message, class: "form-control", rows: 2, required: true
+ = f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url)
.help-block
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index d3afc658cd6..2ab01704b77 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -2,25 +2,30 @@
- user = abuse_report.user
%tr
%td
- - if reporter
- = link_to reporter.name, reporter
+ - if user
+ = link_to user.name, [:admin, user]
+ .light.small
+ Joined #{time_ago_with_tooltip(user.created_at)}
- else
(removed)
%td
- = abuse_report.created_at.to_s(:short)
- %td
- = abuse_report.message
- %td
- - if user
- = link_to user.name, user
+ - if reporter
+ = link_to reporter.name, [:admin, reporter]
- else
(removed)
+ .light.small
+ = time_ago_with_tooltip(abuse_report.created_at)
+ %td
+ = markdown(abuse_report.message.squish!, pipeline: :single_line)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr"
%td
- - if user
+ - if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ - else
+ .btn.btn-xs.disabled
+ Already Blocked
= link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 40a5fe4628b..bc4a9cedb2c 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -6,10 +6,9 @@
%table.table
%thead
%tr
+ %th User
%th Reported by
- %th Reported at
%th Message
- %th User
%th Primary action
%th
= render @abuse_reports
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
new file mode 100644
index 00000000000..6f325914d14
--- /dev/null
+++ b/app/views/admin/appearances/_form.html.haml
@@ -0,0 +1,58 @@
+= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
+ - if @appearance.errors.any?
+ .alert.alert-danger
+ - @appearance.errors.full_messages.each do |msg|
+ %p= msg
+
+ %fieldset.sign-in
+ %legend
+ Sign in/Sign up pages:
+ .form-group
+ = f.label :title, class: 'control-label'
+ .col-sm-10
+ = f.text_field :title, class: "form-control"
+ .form-group
+ = f.label :description, class: 'control-label'
+ .col-sm-10
+ = f.text_area :description, class: "form-control", rows: 10
+ .hint
+ Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown', 'markdown'), target: '_blank'}.
+ .form-group
+ = f.label :logo, class: 'control-label'
+ .col-sm-10
+ - if @appearance.logo?
+ = image_tag @appearance.logo_url, class: 'appearance-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ %hr
+ = f.hidden_field :logo_cache
+ = f.file_field :logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
+
+ %fieldset.app_logo
+ %legend
+ Navigation bar:
+ .form-group
+ = f.label :header_logo, 'Header logo', class: 'control-label'
+ .col-sm-10
+ - if @appearance.header_logo?
+ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ %hr
+ = f.hidden_field :header_logo_cache
+ = f.file_field :header_logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+ - if @appearance.persisted?
+ = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+
+ - if @appearance.updated_at
+ %span.pull-right
+ Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
new file mode 100644
index 00000000000..dd4a64e80bc
--- /dev/null
+++ b/app/views/admin/appearances/preview.html.haml
@@ -0,0 +1,29 @@
+- page_title "Preview | Appearance"
+%h3.page-title
+ Appearance settings - Preview
+%hr
+
+.ui-box
+ .title
+ Sign-in page
+ %div
+ .login-page
+ .container
+ .content
+ .login-title
+ %h1= brand_title
+ %hr
+ .container
+ .content
+ .row
+ .col-sm-7
+ .brand-image
+ = brand_image
+ .brand_text
+ = brand_text
+ .col-sm-4
+ .login-box
+ %h3.page-title Sign in
+ = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
+ = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
+ = button_tag "Sign in", class: "btn-create btn"
diff --git a/app/views/admin/appearances/show.html.haml b/app/views/admin/appearances/show.html.haml
new file mode 100644
index 00000000000..089e8e4cb7a
--- /dev/null
+++ b/app/views/admin/appearances/show.html.haml
@@ -0,0 +1,7 @@
+- page_title "Appearance"
+%h3.page-title
+ Appearance settings
+%p.light
+ You can modify the look and feel of GitLab here
+
+= render 'form'
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 89b38a0dad0..b30dfd109ea 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -14,11 +14,11 @@
.form-group.project-visibility-level-holder
= f.label :default_project_visibility, class: 'control-label col-sm-2'
.col-sm-10
- = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project)
+ = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
.form-group.project-visibility-level-holder
= 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: PersonalSnippet)
+ = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10
@@ -48,6 +48,16 @@
= f.check_box :version_check_enabled
Version check enabled
.form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :email_author_in_body do
+ = f.check_box :email_author_in_body
+ Include author name in notification email body
+ .help-block
+ Some email servers do not support overriding the email sender name.
+ Enable this option to include the name of the author of the issue,
+ merge request or comment in the email body instead.
+ .form-group
= f.label :admin_notification_email, class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :admin_notification_email, class: 'form-control'
@@ -105,14 +115,14 @@
= f.check_box :signin_enabled
Sign-in enabled
.form-group
- = f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2'
+ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
- Require all users to setup Two-Factor authentication
+ Require all users to setup Two-factor authentication
.form-group
- = f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2'
+ = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
@@ -180,14 +190,6 @@
sending messages to this port, without it metrics data will not be
saved.
.form-group
- = f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_username, class: 'form-control'
- .form-group
- = f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_password, class: 'form-control'
- .form-group
= f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :metrics_pool_size, class: 'form-control'
@@ -210,6 +212,13 @@
.help-block
A method call is only tracked when it takes longer to complete than
the given amount of milliseconds.
+ .form-group
+ = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_sample_interval, class: 'form-control'
+ .help-block
+ The sampling interval in seconds. Sampled data includes memory usage,
+ retained Ruby objects, file descriptors and so on.
%fieldset
%legend Spam and Anti-bot Protection
@@ -219,19 +228,55 @@
= f.label :recaptcha_enabled do
= f.check_box :recaptcha_enabled
Enable reCAPTCHA
- %span.help-block#recaptcha_help_block Helps preventing bots from creating accounts
+ %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
.form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_site_key, class: 'form-control'
.help-block
- Generate site and private keys here:
+ Generate site and private keys at
%a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha
+
.form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_private_key, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :akismet_enabled do
+ = f.check_box :akismet_enabled
+ Enable Akismet
+ %span.help-block#akismet_help_block Helps prevent bots from creating issues
+
+ .form-group
+ = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :akismet_api_key, class: 'form-control'
+ .help-block
+ Generate API key at
+ %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
+
+ %fieldset
+ %legend Error Reporting and Logging
+ %p
+ These settings require a restart to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sentry_enabled do
+ = f.check_box :sentry_enabled
+ Enable Sentry
+ .help-block
+ Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
+ %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com
+
+ .form-group
+ = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :sentry_dsn, class: 'form-control'
+
.form-actions
- = f.submit 'Save', class: 'btn btn-primary'
+ = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index fa4e6335c73..e18f7b499dd 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -22,5 +22,5 @@
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
.form-actions
- = f.submit 'Submit', class: "btn btn-primary wide"
+ = f.submit 'Submit', class: "btn btn-save wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-default"
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
new file mode 100644
index 00000000000..b748460a9f7
--- /dev/null
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -0,0 +1,40 @@
+.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
+ = icon('bullhorn')
+ .js-broadcast-message-preview
+ = render_broadcast_message(@broadcast_message.message.presence || "Your message here")
+
+= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
+ -if @broadcast_message.errors.any?
+ .alert.alert-danger
+ - @broadcast_message.errors.full_messages.each do |msg|
+ %p= msg
+ .form-group
+ = f.label :message, class: 'control-label'
+ .col-sm-10
+ = f.text_area :message, class: "form-control js-autosize",
+ required: true,
+ data: { preview_path: preview_admin_broadcast_messages_path }
+ .form-group.js-toggle-colors-container
+ .col-sm-10.col-sm-offset-2
+ = link_to 'Customize colors', '#', class: 'js-toggle-colors-link'
+ .form-group.js-toggle-colors-container.hide
+ = f.label :color, "Background Color", class: 'control-label'
+ .col-sm-10
+ = f.color_field :color, class: "form-control"
+ .form-group.js-toggle-colors-container.hide
+ = f.label :font, "Font Color", class: 'control-label'
+ .col-sm-10
+ = f.color_field :font, class: "form-control"
+ .form-group
+ = f.label :starts_at, class: 'control-label'
+ .col-sm-10.datetime-controls
+ = f.datetime_select :starts_at, {}, class: 'form-control form-control-inline'
+ .form-group
+ = f.label :ends_at, class: 'control-label'
+ .col-sm-10.datetime-controls
+ = f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
+ .form-actions
+ - if @broadcast_message.persisted?
+ = f.submit "Update broadcast message", class: "btn btn-create"
+ - else
+ = f.submit "Add broadcast message", class: "btn btn-create"
diff --git a/app/views/admin/broadcast_messages/edit.html.haml b/app/views/admin/broadcast_messages/edit.html.haml
new file mode 100644
index 00000000000..45e053eb31d
--- /dev/null
+++ b/app/views/admin/broadcast_messages/edit.html.haml
@@ -0,0 +1,3 @@
+- page_title "Broadcast Messages"
+
+= render 'form'
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 17dffebd360..c05538a393c 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -1,60 +1,37 @@
- page_title "Broadcast Messages"
+
%h3.page-title
Broadcast Messages
%p.light
- Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more.
-.broadcast-message-preview
- %i.fa.fa-bullhorn
- %span Your message here
-
-= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal'} do |f|
- -if @broadcast_message.errors.any?
- .alert.alert-danger
- - @broadcast_message.errors.full_messages.each do |msg|
- %p= msg
- .form-group
- = f.label :message, class: 'control-label'
- .col-sm-10
- = f.text_area :message, class: "form-control", rows: 2, required: true
- %div
- = link_to '#', class: 'js-toggle-colors-link' do
- Customize colors
- .form-group.js-toggle-colors-container.hide
- = f.label :color, "Background Color", class: 'control-label'
- .col-sm-10
- = f.color_field :color, value: "#eb9532", class: "form-control"
- .form-group.js-toggle-colors-container.hide
- = f.label :font, "Font Color", class: 'control-label'
- .col-sm-10
- = f.color_field :font, value: "#FFFFFF", class: "form-control"
- .form-group
- = f.label :starts_at, class: 'control-label'
- .col-sm-10.datetime-controls
- = f.datetime_select :starts_at
- .form-group
- = f.label :ends_at, class: 'control-label'
- .col-sm-10.datetime-controls
- = f.datetime_select :ends_at
- .form-actions
- = f.submit "Add broadcast message", class: "btn btn-create"
+ Broadcast messages are displayed for every user and can be used to notify
+ users about scheduled maintenance, recent upgrades and more.
--if @broadcast_messages.any?
- %ul.bordered-list.broadcast-messages
- - @broadcast_messages.each do |broadcast_message|
- %li
- .pull-right
- - if broadcast_message.starts_at
- %strong
- #{broadcast_message.starts_at.to_s(:short)}
- \...
- - if broadcast_message.ends_at
- %strong
- #{broadcast_message.ends_at.to_s(:short)}
- &nbsp;
- = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-xs' do
- %i.fa.fa-times.cred
+= render 'form'
- .message= broadcast_message.message
+%br.clearfix
+-if @broadcast_messages.any?
+ %table.table
+ %thead
+ %tr
+ %th Status
+ %th Preview
+ %th Starts
+ %th Ends
+ %th &nbsp;
+ %tbody
+ - @broadcast_messages.each do |message|
+ %tr
+ %td
+ = broadcast_message_status(message)
+ %td
+ = broadcast_message(message)
+ %td
+ = message.starts_at
+ %td
+ = message.ends_at
+ %td
+ = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-xs'
+ = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-xs btn-danger'
- = paginate @broadcast_messages
+ = paginate @broadcast_messages, theme: 'gitlab'
diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml
new file mode 100644
index 00000000000..fbc9453c72e
--- /dev/null
+++ b/app/views/admin/broadcast_messages/preview.js.haml
@@ -0,0 +1 @@
+$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}");
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 6936e614346..588ad767426 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -4,13 +4,13 @@
= ci_status_with_icon(build.status)
%td.build-link
- - if build.target_url
- = link_to build.target_url do
+ - if can?(current_user, :read_build, build.project)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%strong Build ##{build.id}
- else
%strong Build ##{build.id}
- - if build.show_warning?
+ - if build.stuck?
%i.fa.fa-warning.text-warning
%td
@@ -18,11 +18,11 @@
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
%td
- = link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace"
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
%td
- if build.ref
- = link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref)
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- else
.light none
@@ -60,14 +60,13 @@
%td
.pull-right
- - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url
- = link_to build.download_url, title: 'Download artifacts' do
+ - if can?(current_user, :read_build, project) && build.artifacts?
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
%i.fa.fa-download
- - if current_user && can?(current_user, :manage_builds, build.project)
+ - if can?(current_user, :update_build, build.project)
- if build.active?
- - if build.cancel_url
- = link_to build.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && build.retry_url
- = link_to build.retry_url, method: :post, title: 'Retry' do
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
%i.fa.fa-repeat
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 55da06a7fe9..5931efdefe6 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -1,26 +1,25 @@
-.project-issuable-filter
- .controls
- .pull-left.hidden-xs
- - if @all_builds.running_or_pending.any?
- = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
-
- %ul.center-top-menu
+.top-area
+ %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do
+ All
+ %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to admin_builds_path(scope: :running) do
Running
- %span.badge.js-running-count= @all_builds.running_or_pending.count(:id)
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id))
%li{class: ('active' if @scope == 'finished')}
= link_to admin_builds_path(scope: :finished) do
Finished
- %span.badge.js-running-count= @all_builds.finished.count(:id)
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id))
- %li{class: ('active' if @scope == 'all')}
- = link_to admin_builds_path(scope: :all) do
- All
- %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+ .nav-controls
+ - if @all_builds.running_or_pending.any?
+ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
-.gray-content-block
+.gray-content-block.second-block
#{(@scope || 'running').capitalize} builds
%ul.content-list
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 531247e9148..3274ba5377b 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -6,35 +6,35 @@
%p
Forks
%span.light.pull-right
- = ForkedProjectLink.count
+ = number_with_delimiter(ForkedProjectLink.count)
%p
Issues
%span.light.pull-right
- = Issue.count
+ = number_with_delimiter(Issue.count)
%p
Merge Requests
%span.light.pull-right
- = MergeRequest.count
+ = number_with_delimiter(MergeRequest.count)
%p
Notes
%span.light.pull-right
- = Note.count
+ = number_with_delimiter(Note.count)
%p
Snippets
%span.light.pull-right
- = Snippet.count
+ = number_with_delimiter(Snippet.count)
%p
SSH Keys
%span.light.pull-right
- = Key.count
+ = number_with_delimiter(Key.count)
%p
Milestones
%span.light.pull-right
- = Milestone.count
+ = number_with_delimiter(Milestone.count)
%p
Active Users
%span.light.pull-right
- = User.active.count
+ = number_with_delimiter(User.active.count)
.col-md-4
%h4
Features
@@ -92,6 +92,11 @@
Rails
%span.pull-right
#{Rails::VERSION::STRING}
+
+ %p
+ = Gitlab::Database.adapter_name
+ %span.pull-right
+ = Gitlab::Database.version
%hr
.row
.col-sm-4
@@ -99,7 +104,7 @@
%h4 Projects
.data
= link_to admin_namespaces_projects_path do
- %h1= Project.count
+ %h1= number_with_delimiter(Project.count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
.col-sm-4
@@ -107,7 +112,7 @@
%h4 Users
.data
= link_to admin_users_path do
- %h1= User.count
+ %h1= number_with_delimiter(User.count)
%hr
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -115,7 +120,7 @@
%h4 Groups
.data
= link_to admin_groups_path do
- %h1= Group.count
+ %h1= number_with_delimiter(Group.count)
%hr
= link_to 'New Group', new_admin_group_path, class: "btn btn-new"
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 841e6971fb2..41c43899978 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -2,7 +2,7 @@
.panel.panel-default
.panel-heading
Public deploy keys (#{@deploy_keys.count})
- .panel-head-actions
+ .controls
= link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm"
- if @deploy_keys.any?
.table-holder
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 8de2ba74a79..198026a1f75 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -21,6 +21,5 @@
- else
.form-actions
- = f.submit 'Save changes', class: "btn btn-primary"
+ = f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
-
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 5ce7cdf2f8d..118d3cfea07 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Groups"
%h3.page-title
- Groups (#{@groups.total_count})
+ Groups (#{number_with_delimiter(@groups.total_count)})
= link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right"
%p.light
@@ -17,7 +17,7 @@
.pull-right
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %span.light sort:
+ %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 296497a4cd4..264fa1bf0cd 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -30,7 +30,7 @@
%li
%span.light Created on:
%strong
- = @group.created_at.stamp("March 1, 1999")
+ = @group.created_at.to_s(:medium)
.panel.panel-default
.panel-heading
@@ -50,6 +50,22 @@
.panel-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
+ - if @group.shared_projects.any?
+ .panel.panel-default
+ .panel-heading
+ Projects shared with #{@group.name}
+ %span.badge
+ #{@group.shared_projects.count}
+ %ul.well-list
+ - @group.shared_projects.sort_by(&:name).each do |project|
+ %li
+ %strong
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ %span.label.label-gray
+ = repository_size(project)
+ %span.pull-right.light
+ %span.monospace= project.path_with_namespace + ".git"
+
.col-md-6
- if can?(current_user, :admin_group_member, @group)
.panel.panel-default
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index b120f4dea67..53b3cd04c68 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -37,8 +37,7 @@
- @hooks.each do |hook|
%li
.list-item-name
- = link_to admin_hook_path(hook) do
- %strong= hook.url
+ %strong= hook.url
%p SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
.pull-right
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index eaa94ed9e36..8c6b389bf15 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -12,6 +12,10 @@
.col-sm-10
= f.text_field :title, class: "form-control", required: true
.form-group
+ = f.label :description, class: 'control-label'
+ .col-sm-10
+ = f.text_field :description, class: "form-control js-quick-submit"
+ .form-group
= f.label :color, "Background color", class: 'control-label'
.col-sm-10
.input-group
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index e3ccbf6c3a8..5736a301910 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,5 +1,7 @@
%li{id: dom_id(label)}
- = render_colored_label(label)
- .pull-right
- = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
- = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
+ .label-row
+ = render_colored_label(label)
+ = markdown(label.description, pipeline: :single_line)
+ .pull-right
+ = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
+ = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index d67454c03e7..3c57e3dc174 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,5 +1,5 @@
- page_title "Labels"
-= link_to new_admin_label_path, class: "pull-right btn btn-new" do
+= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
New label
%h3.page-title
Labels
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 1484baa78e0..af9fdeb0734 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,12 +1,13 @@
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
-%ul.nav.nav-tabs.log-tabs
+%ul.nav-links.log-tabs
- loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
-%p.light To prevent performance issues admin logs output the last 2000 lines
+.gray-content-block
+ To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index d9b481404f7..d39c0f44031 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,7 +1,7 @@
- page_title "Projects"
= render 'shared/show_aside'
-.row
+.row.prepend-top-default
%aside.col-md-3
.admin-filter
= form_tag admin_namespaces_projects_path, method: :get, class: '' do
@@ -47,10 +47,10 @@
.panel.panel-default
.panel-heading
Projects (#{@projects.total_count})
- .panel-head-actions
+ .controls
.dropdown.inline
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
+ %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 5260eadf95b..d734e60682a 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,7 +1,7 @@
- page_title @project.name_with_namespace, "Projects"
%h3.page-title
Project: #{@project.name_with_namespace}
- = link_to edit_project_path(@project), class: "btn pull-right" do
+ = link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
@@ -38,7 +38,7 @@
%li
%span.light Created on:
%strong
- = @project.created_at.stamp("March 1, 1999")
+ = @project.created_at.to_s(:medium)
%li
%span.light http:
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
new file mode 100644
index 00000000000..8aea67f4497
--- /dev/null
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -0,0 +1,32 @@
+- user = spam_log.user
+%tr
+ %td
+ = time_ago_with_tooltip(spam_log.created_at)
+ %td
+ - if user
+ = link_to user.name, [:admin, user]
+ .light.small
+ Joined #{time_ago_with_tooltip(user.created_at)}
+ - else
+ (removed)
+ %td
+ = spam_log.source_ip
+ %td
+ = spam_log.via_api? ? 'Y' : 'N'
+ %td
+ = spam_log.noteable_type
+ %td
+ = spam_log.title
+ %td
+ = truncate(spam_log.description, length: 100)
+ %td
+ - if user
+ = link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
+ data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
+ %td
+ - if user && !user.blocked?
+ = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ - else
+ .btn.btn-xs.disabled
+ Already Blocked
+ = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
new file mode 100644
index 00000000000..0fdd5bd9960
--- /dev/null
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -0,0 +1,21 @@
+- page_title "Spam Logs"
+%h3.page-title Spam Logs
+%hr
+- if @spam_logs.present?
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Date
+ %th User
+ %th Source IP
+ %th API?
+ %th Type
+ %th Title
+ %th Description
+ %th Primary Action
+ %th
+ = render @spam_logs
+ = paginate @spam_logs
+- else
+ %h4 There are no Spam Logs
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index e18dd9bc905..d2527ede995 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -58,9 +58,15 @@
= f.label :admin, class: 'control-label'
- if current_user == @user
.col-sm-10= f.check_box :admin, disabled: true
- .col-sm-10 You cannot remove your own admin rights
+ .col-sm-10 You cannot remove your own admin rights.
- else
.col-sm-10= f.check_box :admin
+
+ .form-group
+ = f.label :external, class: 'control-label'
+ .col-sm-10= f.check_box :external
+ .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
+
%fieldset
%legend Profile
.form-group
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 5e17b018163..ce5e21e54cc 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -7,12 +7,12 @@
.pull-right
- unless @user == current_user || @user.blocked?
- = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info"
- = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do
+ = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
+ = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
%hr
-%ul.nav.nav-tabs
+%ul.nav-links
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#groups') do
@@ -23,3 +23,4 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
+.append-bottom-default
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index 7d11edc79e2..6bc217f84cc 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -4,7 +4,7 @@
%ul.well-list
%li
%span.light Member since
- %strong= user.created_at.stamp("Aug 21, 2011")
+ %strong= user.created_at.to_s(:medium)
- unless user.public_email.blank?
%li
%span.light E-mail:
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index bc08458312c..0ee8dc962b9 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,101 +1,107 @@
- page_title "Users"
= render 'shared/show_aside'
-.row
- %aside.col-md-3
- .admin-filter
- %ul.nav.nav-pills.nav-stacked
- %li{class: "#{'active' unless params[:filter]}"}
- = link_to admin_users_path do
- Active
- %small.pull-right= User.active.count
- %li{class: "#{'active' if params[:filter] == "admins"}"}
- = link_to admin_users_path(filter: "admins") do
- Admins
- %small.pull-right= User.admins.count
- %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
- = link_to admin_users_path(filter: 'two_factor_enabled') do
- 2FA Enabled
- %small.pull-right= User.with_two_factor.count
- %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
- = link_to admin_users_path(filter: 'two_factor_disabled') do
- 2FA Disabled
- %small.pull-right= User.without_two_factor.count
- %li{class: "#{'active' if params[:filter] == "blocked"}"}
- = link_to admin_users_path(filter: "blocked") do
- Blocked
- %small.pull-right= User.blocked.count
- %li{class: "#{'active' if params[:filter] == "wop"}"}
- = link_to admin_users_path(filter: "wop") do
- Without projects
- %small.pull-right= User.without_projects.count
- %hr
- = form_tag admin_users_path, method: :get, class: 'form-inline' do
- .form-group
- = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
- = hidden_field_tag "filter", params[:filter]
- = button_tag class: 'btn btn-primary' do
- %i.fa.fa-search
- %hr
- = link_to 'Reset', admin_users_path, class: "btn btn-cancel"
+.admin-filter
+ %ul.nav-links
+ %li{class: "#{'active' unless params[:filter]}"}
+ = link_to admin_users_path do
+ Active
+ %small.badge= number_with_delimiter(User.active.count)
+ %li{class: "#{'active' if params[:filter] == "admins"}"}
+ = link_to admin_users_path(filter: "admins") do
+ Admins
+ %small.badge= number_with_delimiter(User.admins.count)
+ %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_enabled') do
+ 2FA Enabled
+ %small.badge= number_with_delimiter(User.with_two_factor.count)
+ %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_disabled') do
+ 2FA Disabled
+ %small.badge= number_with_delimiter(User.without_two_factor.count)
+ %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"}
+ = link_to admin_users_path(filter: 'external') do
+ External
+ %small.badge= number_with_delimiter(User.external.count)
+ %li{class: "#{'active' if params[:filter] == "blocked"}"}
+ = link_to admin_users_path(filter: "blocked") do
+ Blocked
+ %small.badge= number_with_delimiter(User.blocked.count)
+ %li{class: "#{'active' if params[:filter] == "wop"}"}
+ = link_to admin_users_path(filter: "wop") do
+ Without projects
+ %small.badge= number_with_delimiter(User.without_projects.count)
- %section.col-md-9
- .panel.panel-default
- .panel-heading
- Users (#{@users.total_count})
- .panel-head-actions
- .dropdown.inline
- %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
- %span.light sort:
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_name
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
- = sort_title_name
- = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
- = sort_title_recently_signin
- = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
- = sort_title_oldest_signin
- = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
- = sort_title_recently_created
- = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
- = sort_title_oldest_created
- = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
- = sort_title_recently_updated
- = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
- = sort_title_oldest_updated
-
- = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm"
- %ul.well-list
- - @users.each do |user|
+ .gray-content-block.second-block
+ .pull-right
+ .dropdown.inline
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_name
+ %b.caret
+ %ul.dropdown-menu
%li
- .list-item-name
- - if user.blocked?
- %i.fa.fa-lock.cred
+ = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
+ = sort_title_name
+ = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
+ = sort_title_recently_signin
+ = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
+ = sort_title_oldest_signin
+ = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
+ = sort_title_recently_created
+ = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
+ = sort_title_oldest_created
+ = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
+ = sort_title_recently_updated
+ = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
+ = sort_title_oldest_updated
+
+ = link_to 'New User', new_admin_user_path, class: "btn btn-new"
+ = form_tag admin_users_path, method: :get, class: 'form-inline' do
+ .form-group
+ = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
+ = hidden_field_tag "filter", params[:filter]
+ = button_tag class: 'btn btn-primary' do
+ %i.fa.fa-search
+
+
+.panel.panel-default
+ %ul.well-list
+ - @users.each do |user|
+ %li
+ .list-item-name
+ - if user.blocked?
+ = icon("lock", class: "cred")
+ - else
+ = icon("user", class: "cgreen")
+ = link_to user.name, [:admin, user]
+ - if user.admin?
+ %strong.cred (Admin)
+ - if user.external?
+ %strong.cred (External)
+ - if user == current_user
+ %span.cred It's you!
+ .pull-right
+ %span.light
+ %i.fa.fa-envelope
+ = mail_to user.email, user.email, class: 'light'
+ &nbsp;
+ .pull-right
+ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
+ - unless user == current_user
+ - if user.ldap_blocked?
+ = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
+ %i.fa.fa-lock
+ Unblock
+ - elsif user.blocked?
+ = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else
- %i.fa.fa-user.cgreen
- = link_to user.name, [:admin, user]
- - if user.admin?
- %strong.cred (Admin)
- - if user == current_user
- %span.cred It's you!
- .pull-right
- %span.light
- %i.fa.fa-envelope
- = mail_to user.email, user.email, class: 'light'
- &nbsp;
- = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs"
- - unless user == current_user
- - if user.blocked?
- = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success"
- - else
- = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning"
- - if user.access_locked?
- = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' }
- - if user.can_be_removed?
- = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
- = paginate @users, theme: "gitlab"
+ = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
+ - if user.access_locked?
+ = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
+ - if user.can_be_removed?
+ = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
+= paginate @users, theme: "gitlab"
diff --git a/app/views/admin/users/keys.html.haml b/app/views/admin/users/keys.html.haml
index 07110717082..0f644121e62 100644
--- a/app/views/admin/users/keys.html.haml
+++ b/app/views/admin/users/keys.html.haml
@@ -1,3 +1,3 @@
-- page_title "Keys", @user.name, "Users"
+- page_title "SSH Keys", @user.name, "Users"
= render 'admin/users/head'
= render 'profiles/keys/key_table', admin: true
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 0848504b7a6..d37489bebea 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -48,6 +48,10 @@
Disabled
%li
+ %span.light External User:
+ %strong
+ = @user.external? ? "Yes" : "No"
+ %li
%span.light Can create groups:
%strong
= @user.can_create_group ? "Yes" : "No"
@@ -58,12 +62,12 @@
%li
%span.light Member since:
%strong
- = @user.created_at.stamp("Nov 12, 2031")
+ = @user.created_at.to_s(:medium)
- if @user.confirmed_at
%li
%span.light Confirmed at:
%strong
- = @user.confirmed_at.stamp("Nov 12, 2031")
+ = @user.confirmed_at.to_s(:medium)
- else
%li
%span.light Confirmed:
@@ -71,10 +75,26 @@
No
%li
+ %span.light Current sign-in IP:
+ %strong
+ - if @user.current_sign_in_ip
+ = @user.current_sign_in_ip
+ - else
+ never
+
+ %li
%span.light Current sign-in at:
%strong
- if @user.current_sign_in_at
- = @user.current_sign_in_at.stamp("Nov 12, 2031")
+ = @user.current_sign_in_at.to_s(:medium)
+ - else
+ never
+
+ %li
+ %span.light Last sign-in IP:
+ %strong
+ - if @user.last_sign_in_ip
+ = @user.last_sign_in_ip
- else
never
@@ -82,7 +102,7 @@
%span.light Last sign-in at:
%strong
- if @user.last_sign_in_at
- = @user.last_sign_in_at.stamp("Nov 12, 2031")
+ = @user.last_sign_in_at.to_s(:medium)
- else
never
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
deleted file mode 100644
index 11163813f3e..00000000000
--- a/app/views/ci/commits/_commit.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-%tr.build
- %td.status
- = ci_status_with_icon(commit.status)
- - if commit.running?
- &middot;
- = commit.stage
-
-
- %td.build-link
- = link_to ci_status_path(commit) do
- %strong #{commit.short_sha}
-
- %td.build-message
- %span= truncate_first_line(commit.git_commit_message)
-
- %td.build-branch
- - unless @ref
- %span
- - commit.refs.each do |ref|
- = link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref)
-
- %td.duration
- - if commit.duration > 0
- #{time_interval_in_words commit.duration}
-
- %td.timestamp
- - if commit.finished_at
- %span #{time_ago_in_words commit.finished_at} ago
-
- - if commit.coverage
- %td.coverage
- #{commit.coverage}%
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index a144c43be47..0044d779c31 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -4,12 +4,12 @@
.row
= form_tag ci_lint_path, method: :post do
.form-group
- = label_tag :content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap'
+ = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap')
.col-sm-12
- = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
+ = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true)
.col-sm-12
.pull-left.prepend-top-10
- = submit_tag 'Validate', class: 'btn btn-success submit-yml'
+ = submit_tag('Validate', class: 'btn btn-success submit-yml')
.row.prepend-top-20
.col-sm-12
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index f98fd9f06ba..dc76599b776 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,9 +1,9 @@
.hidden-xs
= render "events/event_last_push", event: @last_push
-.gray-content-block
+.nav-block
- if current_user
- .pull-right
+ .controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 9f4be025bf2..b78e70ebc1e 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
%li{ class: ("active" unless params[:filter]) }
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 64bd356f546..3d17f74b709 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,7 +1,13 @@
-%ul.center-top-menu
- = nav_link(page: dashboard_groups_path) do
- = link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do
- Your Groups
- = nav_link(page: explore_groups_path) do
- = link_to explore_groups_path, title: 'Explore groups', data: {placement: 'bottom'} do
- Explore Groups
+.top-area
+ %ul.nav-links
+ = nav_link(page: dashboard_groups_path) do
+ = link_to dashboard_groups_path, title: 'Your groups' do
+ Your Groups
+ = nav_link(page: explore_groups_path) do
+ = link_to explore_groups_path, title: 'Explore groups' do
+ Explore Groups
+ - if current_user.can_create_group?
+ .nav-controls
+ = link_to new_group_path, class: "btn btn-new" do
+ = icon('plus')
+ New Group
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index f4a3e3162bf..9da3fcbd986 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,20 +1,22 @@
= content_for :flash_message do
= render 'shared/project_limit'
.top-area
- %ul.left-top-menu
+ %ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
Starred Projects
- = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do
+ = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
Explore Projects
- .projects-search-form
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name...', class: 'projects-list-filter form-control hidden-xs', spellcheck: false
+ .nav-controls
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
+ = render 'shared/projects/dropdown'
- if current_user.can_create_project?
- = link_to new_project_path, class: 'btn btn-green' do
- %i.fa.fa-plus
+ = link_to new_project_path, class: 'btn btn-new' do
+ = icon('plus')
New Project
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index 0ae62d6f1b6..b25e8ea1f0c 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index d5b7e729e7b..caca91af536 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -2,15 +2,6 @@
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
-.gray-content-block
- - if current_user.can_create_group?
- %span.pull-right.hidden-xs
- = link_to new_group_path, class: "btn btn-new" do
- %i.fa.fa-plus
- New Group
- .oneline
- Group members have access to all group projects.
-
%ul.content-list
- @group_members.each do |group_member|
- group = group_member.group
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 07bda1c77f8..0d7b1b30dc3 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 2d3da01178a..dfa5f80eef8 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,20 +4,15 @@
- if current_user
= auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
-.project-issuable-filter
- .controls
- .pull-left
- - if current_user
- .hidden-xs.pull-left
- = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
- %i.fa.fa-rss
-
+.top-area
+ = render 'shared/issuable/nav', type: :issues
+ .nav-controls
+ - if current_user
+ = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
+ = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
- = render 'shared/issuable/filter', type: :issues
-
-.gray-content-block.second-block
- List all issues from all projects you have access to.
+= render 'shared/issuable/filter', type: :issues
.prepend-top-default
= render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index c5a5ec21f78..fb016599fef 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,14 +1,12 @@
- page_title "Merge Requests"
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
-.project-issuable-filter
- .controls
+.top-area
+ = render 'shared/issuable/nav', type: :merge_requests
+ .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
- = render 'shared/issuable/filter', type: :merge_requests
-
-.gray-content-block.second-block
- List all merge requests from all projects you have access to.
+= render 'shared/issuable/filter', type: :merge_requests
.prepend-top-default
= render 'shared/merge_requests'
diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml
deleted file mode 100644
index 1408ebdd5dc..00000000000
--- a/app/views/dashboard/milestones/_issue.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
- %span.milestone-row
- - project = issue.project
- %strong #{project.name_with_namespace} &middot;
- = link_to [project.namespace.becomes(Namespace), project, issue] do
- %span.cgray ##{issue.iid}
- = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
- .pull-right.assignee-icon
- - if issue.assignee
- = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16"
diff --git a/app/views/dashboard/milestones/_issues.html.haml b/app/views/dashboard/milestones/_issues.html.haml
deleted file mode 100644
index 9f350b772bd..00000000000
--- a/app/views/dashboard/milestones/_issues.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list issues-sortable-list" }
- - if issues
- - issues.each do |issue|
- = render 'issue', issue: issue
diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml
deleted file mode 100644
index 77c46de030b..00000000000
--- a/app/views/dashboard/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
- %span.milestone-row
- - project = merge_request.project
- %strong #{project.name_with_namespace} &middot;
- = link_to [project.namespace.becomes(Namespace), project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
- .pull-right.assignee-icon
- - if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16"
diff --git a/app/views/dashboard/milestones/_merge_requests.html.haml b/app/views/dashboard/milestones/_merge_requests.html.haml
deleted file mode 100644
index 50057e2c636..00000000000
--- a/app/views/dashboard/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list" }
- - if merge_requests
- - merge_requests.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index 7c882a32702..6173ca6ab9b 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -1,25 +1,6 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to issues_dashboard_path(milestone_title: milestone.title) do
- = pluralize milestone.issue_count, 'Issue'
- &middot;
- = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do
- = pluralize milestone.merge_requests_count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
- .row
- .col-sm-6
- .expiration
- = render 'shared/milestone_expired', milestone: milestone
- .projects
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name_with_namespace
+= render 'shared/milestones/milestone',
+ milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title),
+ issues_path: issues_dashboard_path(milestone_title: milestone.title),
+ merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
+ milestone: milestone,
+ dashboard: true
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index bec1692a4de..917bfbd47e9 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,14 +1,11 @@
- page_title "Milestones"
- header_title "Milestones", dashboard_milestones_path
-.project-issuable-filter
- .controls
- = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
-
+.top-area
= render 'shared/milestones_filter'
-.gray-content-block
- List all milestones from all projects you have access to.
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
.milestones
%ul.content-list
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 4316c358dcb..60c84a26420 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -1,105 +1,5 @@
-- page_title @milestone.title, "Milestones"
- header_title "Milestones", dashboard_milestones_path
-.detail-page-header
- .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- - if @milestone.closed?
- Closed
- - else
- Open
- %span.identifier
- Milestone #{@milestone.title}
-
-.detail-page-description.gray-content-block.second-block
- %h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
-
-- if @milestone.complete? && @milestone.active?
- .alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. You may close the milestone now.
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Project
- %th Open issues
- %th State
- %th Due date
- - @milestone.milestones.each do |milestone|
- %tr
- %td
- = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- %td
- = milestone.issues.opened.count
- %td
- - if milestone.closed?
- Closed
- - else
- Open
- %td
- = milestone.expires_at
-
-.context
- %p.lead
- Progress:
- #{@milestone.closed_items_count} closed
- &ndash;
- #{@milestone.open_items_count} open
- = milestone_progress_bar(@milestone)
-
-%ul.center-top-menu.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab' do
- Issues
- %span.badge= @milestone.issue_count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
- Merge Requests
- %span.badge= @milestone.merge_requests_count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @milestone.participants.count
-
-.tab-content
- .tab-pane.active#tab-issues
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All issues in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'issues', title: "Open", issues: @milestone.opened_issues
- .col-md-6
- = render 'issues', title: "Closed", issues: @milestone.closed_issues
-
- .tab-pane#tab-merge-requests
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All merge requests in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
- .col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
-
- .tab-pane#tab-participants
- .gray-content-block.middle-block
- .oneline
- All participants to this milestone
- %ul.bordered-list
- - @milestone.participants.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
+= render 'shared/milestones/top', milestone: @milestone
+= render 'shared/milestones/summary', milestone: @milestone
+= render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index cea9ffcc748..0ebd7c01bab 100644
--- a/app/views/dashboard/projects/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -1,3 +1 @@
-.projects-list-holder
-
- = render 'shared/projects/list', projects: @projects, ci: true
+= render 'shared/projects/list', projects: @projects, ci: true
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index 4e7d6639727..d54c7cad7be 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-- publicish_project_count = Project.publicish(current_user).count
+- publicish_project_count = ProjectsFinder.new.execute(current_user).count
%h3.page-title Welcome to GitLab!
%p.light Self hosted Git management application.
%hr
@@ -11,14 +11,14 @@
%br
- if current_user.can_create_project?
You can create up to
- %strong= pluralize(current_user.projects_limit, "project") + "."
+ %strong= pluralize(number_with_delimiter(current_user.projects_limit), "project") + "."
- else
If you are added to a project, it will be displayed here.
- if current_user.can_create_project?
.link_holder
= link_to new_project_path, class: "btn btn-new" do
- %i.fa.fa-plus
+ = icon('plus')
New Project
- if current_user.can_create_group?
@@ -44,7 +44,7 @@
.dashboard-intro-text
%p.slead
There are
- %strong= publicish_project_count
+ %strong= number_with_delimiter(publicish_project_count)
public projects on this server.
%br
Public projects are an easy way to allow everyone to have read-only access.
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index c8c219f4cca..d4daf07c6c0 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events[0].updated_at.xmlschema if @events[0]
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 53abf274bdb..4565e752c1f 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -10,7 +10,7 @@
- if @last_push
= render "events/event_last_push", event: @last_push
-- if @projects.any?
+- if @projects.any? || params[:filter_projects]
= render 'projects'
- else
= render "zero_authorized_projects"
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 07b6d57932e..d4e7862981c 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -3,32 +3,36 @@
= render 'dashboard/snippets_head'
-.gray-content-block
- .pull-right
+.nav-block
+ .controls
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
= icon('plus')
New Snippet
- .btn-group.btn-group-next.snippet-scope-menu
- = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
- All
- %span.badge
- = current_user.snippets.count
-
- = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
- Private
- %span.badge
- = current_user.snippets.are_private.count
-
- = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
- Internal
- %span.badge
- = current_user.snippets.are_internal.count
-
- = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
- Public
- %span.badge
- = current_user.snippets.are_public.count
+ .nav-links.snippet-scope-menu
+ %li{ class: ("active" unless params[:scope]) }
+ = link_to dashboard_snippets_path do
+ All
+ %span.badge
+ = current_user.snippets.count
+
+ %li{ class: ("active" if params[:scope] == "are_private") }
+ = link_to dashboard_snippets_path(scope: 'are_private') do
+ Private
+ %span.badge
+ = current_user.snippets.are_private.count
+
+ %li{ class: ("active" if params[:scope] == "are_internal") }
+ = link_to dashboard_snippets_path(scope: 'are_internal') do
+ Internal
+ %span.badge
+ = current_user.snippets.are_internal.count
+
+ %li{ class: ("active" if params[:scope] == "are_public") }
+ = link_to dashboard_snippets_path(scope: 'are_public') do
+ Public
+ %span.badge
+ = current_user.snippets.are_public.count
= render 'snippets/snippets'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
new file mode 100644
index 00000000000..4c848a50181
--- /dev/null
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -0,0 +1,26 @@
+%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
+ .todo-item.todo-block
+ = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
+
+ .todo-title
+ %span.author-name
+ - if todo.author
+ = link_to_author(todo)
+ - else
+ (removed)
+ %span.todo-label
+ = todo_action_name(todo)
+ = todo_target_link(todo)
+
+ &middot; #{time_ago_with_tooltip(todo.created_at)}
+
+ - if todo.pending?
+ .todo-actions.pull-right
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ Done
+ = icon('spinner spin')
+
+ .todo-body
+ .todo-note
+ .md
+ = event_note(todo.body, project: todo.project)
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
new file mode 100644
index 00000000000..623381375a5
--- /dev/null
+++ b/app/views/dashboard/todos/index.html.haml
@@ -0,0 +1,66 @@
+- page_title "Todos"
+- header_title "Todos", dashboard_todos_path
+
+.top-area
+ %ul.nav-links
+ - 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
+ - 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
+ %span{class: 'badge'}
+ = todos_done_count
+
+ .nav-controls
+ - if @todos.any?(&:pending?)
+ = 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
+ = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
+ .filter-item.inline
+ = select_tag('project_id', todo_projects_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Project'})
+ .filter-item.inline
+ = users_select_tag(:author_id, selected: params[:author_id],
+ placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ .filter-item.inline
+ = select_tag('type', todo_types_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Type'})
+ .filter-item.inline.actions-filter
+ = select_tag('action_id', todo_actions_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Action'})
+
+.prepend-top-default
+ - if @todos.any?
+ - @todos.group_by(&:project).each do |group|
+ .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
+ = render group[1]
+ = paginate @todos, theme: "gitlab"
+ - else
+ .nothing-here-block You're all done!
+
+:javascript
+ new UsersSelect();
+
+ $('form.filter-form').on('submit', function (event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '&' + $(this).serialize());
+ });
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index dbc8eda6196..d65fa60025c 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,10 +1,10 @@
- page_title "Sign in"
%div
- - if signin_enabled? || ldap_enabled?
+ - if signin_enabled? || ldap_enabled? || crowd_enabled?
= render 'devise/shared/signin_box'
-# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
- - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?
+ - if omniauth_enabled? && devise_mapping.omniauthable?
.clearfix.prepend-top-20
= render 'devise/shared/omniauth_box'
@@ -14,6 +14,6 @@
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- - if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?)
+ - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 41ad2c231d4..2c15e2c4891 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -7,7 +7,7 @@
%h3 Sign in
.login-body
- if form_based_providers.any?
- %ul.nav.nav-tabs
+ %ul.nav-links
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml
index 6a5c917049d..001a711b1dd 100644
--- a/app/views/doorkeeper/applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/applications/_delete_form.html.haml
@@ -1,4 +1,10 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag oauth_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
+ - if defined? small
+ = button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do
+ %span.sr-only
+ Destroy
+ = icon('trash')
+ - else
+ = submit_tag 'Destroy', data: { confirm: "Are you sure?" }, class: submit_btn_css
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 98a61ab211b..906b0676150 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f|
+= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
- if application.errors.any?
.alert.alert-danger
%ul
@@ -6,25 +6,20 @@
%li= msg
.form-group
- = f.label :name, class: 'control-label'
-
- .col-sm-10
- = f.text_field :name, class: 'form-control', required: true
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: 'form-control', required: true
.form-group
- = f.label :redirect_uri, class: 'control-label'
-
- .col-sm-10
- = f.text_area :redirect_uri, class: 'form-control', required: true
+ = f.label :redirect_uri, class: 'label-light'
+ = f.text_area :redirect_uri, class: 'form-control', required: true
+ %span.help-block
+ Use one line per URI
+ - if Doorkeeper.configuration.native_redirect_uri
%span.help-block
- Use one line per URI
- - if Doorkeeper.configuration.native_redirect_uri
- %span.help-block
- Use
- %code= Doorkeeper.configuration.native_redirect_uri
- for local tests
+ Use
+ %code= Doorkeeper.configuration.native_redirect_uri
+ for local tests
- .form-actions
- = f.submit 'Submit', class: "btn btn-create"
- = link_to "Cancel", applications_profile_path, class: "btn btn-cancel"
+ .prepend-top-default
+ = f.submit 'Save application', class: "btn btn-create"
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index ba4c5b86efb..ea0b66c932b 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -1,19 +1,83 @@
- page_title "Applications"
-%h3.page-title Your applications
-%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
+- header_title page_title, applications_profile_path
-.table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td= application.redirect_uri
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
- %td= render 'delete_form', application: application
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ - if user_oauth_applications?
+ Manage applications that can use GitLab as an OAuth provider,
+ and applications that you've authorized to use your account.
+ - else
+ Manage applications that you've authorized to use your account.
+ .col-lg-9
+ - if user_oauth_applications?
+ %h5.prepend-top-0
+ Add new application
+ = render 'form', application: @application
+ %hr
+ - if user_oauth_applications?
+ .oauth-applications
+ %h5
+ Your applications (#{@applications.size})
+ - if @applications.any?
+ .table-responsive
+ %table.table
+ %thead
+ %tr
+ %th Name
+ %th Callback URL
+ %th Clients
+ %th.last-heading
+ %tbody
+ - @applications.each do |application|
+ %tr{id: "application_#{application.id}"}
+ %td= link_to application.name, oauth_application_path(application)
+ %td
+ - application.redirect_uri.split.each do |uri|
+ %div= uri
+ %td= application.access_tokens.count
+ %td
+ = link_to edit_oauth_application_path(application), class: "btn btn-transparent append-right-5" do
+ %span.sr-only
+ Edit
+ = icon('pencil')
+ = render 'delete_form', application: application, small: true
+ - else
+ .profile-settings-message.text-center
+ You don't have any applications
+ .oauth-authorized-applications.prepend-top-20.append-bottom-default
+ - if user_oauth_applications?
+ %h5
+ Authorized applications (#{@authorized_tokens.size})
+
+ - if @authorized_tokens.any?
+ .table-responsive
+ %table.table.table-striped
+ %thead
+ %tr
+ %th Name
+ %th Authorized At
+ %th Scope
+ %th
+ %tbody
+ - @authorized_apps.each do |app|
+ - token = app.authorized_tokens.order('created_at desc').first
+ %tr{id: "application_#{app.id}"}
+ %td= app.name
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'delete_form', application: app
+ - @authorized_anonymous_tokens.each do |token|
+ %tr
+ %td
+ Anonymous
+ %div.help-block
+ %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
+ - else
+ .profile-settings-message.text-center
+ You don't have any authorized applications
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 15f9ee266c1..eae80e5210f 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -4,6 +4,15 @@
Authorize
%strong.text-info= @pre_auth.client.name
to use your account?
+
+ - if current_user.admin?
+ .text-warning.prepend-top-20
+ %p
+ = icon("exclamation-triangle fw")
+ You are an admin, which means granting access to
+ %strong #{@pre_auth.client.name}
+ will allow them to interact with GitLab as an admin as well. Proceed with caution.
+
- if @pre_auth.scopes
#oauth-permissions
%p This application will be able to:
@@ -25,4 +34,4 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
- = submit_tag "Deny", class: "btn btn-danger prepend-left-10" \ No newline at end of file
+ = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
new file mode 100644
index 00000000000..3443a8e2307
--- /dev/null
+++ b/app/views/emojis/index.html.haml
@@ -0,0 +1,11 @@
+.emoji-menu
+ .emoji-menu-content
+ = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
+ - AwardEmoji.emoji_by_category.each do |category, emojis|
+ %h5.emoji-menu-title
+ = AwardEmoji::CATEGORIES[category]
+ %ul.clearfix.emoji-menu-list
+ - emojis.each do |emoji|
+ %li.pull-left.text-center.emoji-menu-list-item
+ %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
+ = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index 4ba8b84fd92..dce4081288c 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -1,5 +1,5 @@
%li.commit
.commit-row-title
- = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''
+ = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id])
&middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 46432a92348..2d9d9dd6342 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,10 +1,10 @@
-- if event.proper?
+- if event.proper?(current_user)
.event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
- = cache [event, current_application_settings, "v2.1"] do
- = image_tag avatar_icon(event.author_email, 46), class: "avatar s46", alt:''
+ = cache [event, current_application_settings, "v2.2"] do
+ = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:''
- if event.created_project?
= render "events/event/created_project", event: event
- elsif event.push?
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index ffc37ad6178..5753158c24d 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -1,9 +1,9 @@
- if show_last_push_widget?(event)
- .gray-content-block.clear-block
+ .gray-content-block.clear-block.last-push-widget
.event-last-push
.event-last-push-text
%span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.project.name) do
%strong= event.ref_name
%span at
%strong= link_to_project event.project
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 4ecf1c33d2a..e9e16a7646f 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_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target]
+ %strong= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target]
= event_preposition(event)
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 8bed5cdb9cc..235bd46107e 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -5,7 +5,7 @@
%strong= event.ref_name
- else
%strong
- = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name)
+ = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.target_title)
at
= link_to_project event.project
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index fcb07b04083..8ffca96bb4e 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -18,7 +18,7 @@
.pull-right
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
+ %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml
deleted file mode 100644
index b23a3c1e5c1..00000000000
--- a/app/views/explore/projects/_dropdown.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-.dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
- - if @sort.present?
- = sort_options_hash[@sort]
- - elsif current_page?(trending_explore_projects_path) || current_page?(explore_root_path)
- Trending projects
- - elsif current_page?(starred_explore_projects_path)
- Most stars
- - else
- = sort_title_recently_created
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to trending_explore_projects_path do
- Trending projects
- = link_to starred_explore_projects_path do
- Most stars
- = link_to explore_projects_filter_path(sort: sort_value_recently_created) do
- = sort_title_recently_created
- = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
-
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 28b12c8dca8..cd485da5104 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,49 +1,40 @@
-.pull-left
- = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f|
- .form-group
- = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false
- .form-group
- = button_tag 'Search', class: "btn"
-
-.pull-right.hidden-sm.hidden-xs
- - if current_user
- .dropdown.inline.append-right-10
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %i.fa.fa-globe
- %span.light Visibility:
- - if params[:visibility_level].present?
- = visibility_level_label(params[:visibility_level].to_i)
- - else
+- if current_user
+ .dropdown
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ = icon('globe')
+ %span.light Visibility:
+ - if params[:visibility_level].present?
+ = visibility_level_label(params[:visibility_level].to_i)
+ - else
+ Any
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to filter_projects_path(visibility_level: nil) do
Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to explore_projects_filter_path(visibility_level: nil) do
- Any
- - Gitlab::VisibilityLevel.values.each do |level|
- %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
- = link_to explore_projects_filter_path(visibility_level: level) do
- = visibility_level_icon(level)
- = visibility_level_label(level)
+ - Gitlab::VisibilityLevel.values.each do |level|
+ %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
+ = link_to filter_projects_path(visibility_level: level) do
+ = visibility_level_icon(level)
+ = visibility_level_label(level)
- - if @tags.present?
- .dropdown.inline.append-right-10
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %i.fa.fa-tags
- %span.light Tags:
- - if params[:tag].present?
- = params[:tag]
- - else
+- if @tags.present?
+ .dropdown
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ = icon('tags')
+ %span.light Tags:
+ - if params[:tag].present?
+ = params[:tag]
+ - else
+ Any
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to filter_projects_path(tag: nil) do
Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to explore_projects_filter_path(tag: nil) do
- Any
- - @tags.each do |tag|
- %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
- = link_to explore_projects_filter_path(tag: tag.name) do
- %i.fa.fa-tag
- = tag.name
- = render 'explore/projects/dropdown'
+ - @tags.each do |tag|
+ %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
+ = link_to filter_projects_path(tag: tag.name) do
+ = icon('tag')
+ = tag.name
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
new file mode 100644
index 00000000000..614b5431779
--- /dev/null
+++ b/app/views/explore/projects/_nav.html.haml
@@ -0,0 +1,10 @@
+%ul.nav-links
+ = nav_link(page: [trending_explore_projects_path, explore_root_path]) do
+ = link_to trending_explore_projects_path do
+ Trending
+ = nav_link(page: starred_explore_projects_path) do
+ = link_to starred_explore_projects_path do
+ Most stars
+ = nav_link(page: explore_projects_path) do
+ = link_to explore_projects_path do
+ All
diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml
index 669079e9521..708fbc27f55 100644
--- a/app/views/explore/projects/_projects.html.haml
+++ b/app/views/explore/projects/_projects.html.haml
@@ -1,6 +1 @@
-- if projects.any?
- .public-projects
- = render 'shared/projects/list', projects: projects
-- else
- .nothing-here-block
- No such projects
+= render 'shared/projects/list', projects: projects
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index b9a958fbe7b..42b50481b9d 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -6,7 +6,10 @@
- else
= render 'explore/head'
-.gray-content-block.clearfix.second-block
- = render 'filter'
+.top-area
+ = render 'explore/projects/nav'
+
+ .nav-controls
+ = render 'filter'
+
= render 'projects', projects: @projects
-= paginate @projects, theme: "gitlab"
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 95d46e331f8..ec461755103 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -6,12 +6,5 @@
- else
= render 'explore/head'
-.explore-trending-block
- .gray-content-block.second-block
- .pull-right
- = render 'explore/projects/dropdown'
- .oneline
- %i.fa.fa-star
- See most starred projects
- = render 'projects', projects: @starred_projects
- = paginate @starred_projects, theme: 'gitlab'
+= render 'explore/projects/nav'
+= render 'projects', projects: @projects
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index fa0b718e48b..ec461755103 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -6,11 +6,5 @@
- else
= render 'explore/head'
-.explore-trending-block
- .gray-content-block.second-block
- .pull-right
- = render 'explore/projects/dropdown'
- .oneline
- %i.fa.fa-comments-o
- See most discussed projects for last month
- = render 'projects', projects: @trending_projects
+= render 'explore/projects/nav'
+= render 'projects', projects: @projects
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
new file mode 100644
index 00000000000..dc76599b776
--- /dev/null
+++ b/app/views/groups/_activities.html.haml
@@ -0,0 +1,12 @@
+.hidden-xs
+ = render "events/event_last_push", event: @last_push
+
+.nav-block
+ - if current_user
+ .controls
+ = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
+ %i.fa.fa-rss
+ = render 'shared/event_filter'
+
+.content_list
+= spinner
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index bbafc08435a..cca7dc27b1c 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -1,11 +1 @@
-.projects-list-holder
- .projects-search-form
- .input-group
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- - if can? current_user, :create_projects, @group
- %span.input-group-btn
- = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do
- %i.fa.fa-plus
- New Project
-
- = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: true
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
new file mode 100644
index 00000000000..b1694c919d0
--- /dev/null
+++ b/app/views/groups/_shared_projects.html.haml
@@ -0,0 +1 @@
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
new file mode 100644
index 00000000000..f73e1d9e865
--- /dev/null
+++ b/app/views/groups/activity.html.haml
@@ -0,0 +1,9 @@
+= 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")
+
+- page_title "Activity"
+- header_title group_title(@group, "Activity", activity_group_path(@group))
+
+%section.activities
+ = render 'activities'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 1dea77c2e96..83936d39b16 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,7 +1,6 @@
- header_title group_title(@group, "Settings", edit_group_path(@group))
-- @blank_container = true
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
Group settings
.panel-body
@@ -26,12 +25,12 @@
.form-group
%hr
- = f.label :public, class: 'control-label' do
- Public
+ = f.label :share_with_group_lock, class: 'control-label' do
+ Share with group lock
.col-sm-10
.checkbox
- = f.check_box :public
- %span.descr Make this group public (even if there are no public projects inside this group)
+ = 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/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
index a79a0fcdc8e..60234be8f83 100644
--- a/app/views/groups/group_members/_group_member.html.haml
+++ b/app/views/groups/group_members/_group_member.html.haml
@@ -1,5 +1,6 @@
- user = member.user
- return unless user || member.invite?
+- show_roles = local_assigns.fetch(:show_roles, true)
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)}
@@ -28,7 +29,7 @@
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite
- - if should_user_see_group_roles?(current_user, @group)
+ - if show_roles && should_user_see_group_roles?(current_user, @group)
%span.pull-right
%strong.member-access-level= member.human_access
- if show_controls
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 3361d7e2a8d..e7ab4f2409b 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -4,7 +4,7 @@
.col-sm-10
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
- Search for existing users or invite new ones using their email address.
+ Search for users by name, username, or email, or invite new ones using their email address.
.form-group
= f.label :access_level, "Group Access", class: 'control-label'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 335bf036074..6b7fd5746d6 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,8 +1,7 @@
- page_title "Members"
- header_title group_title(@group, "Members", group_group_members_path(@group))
-- @blank_container = true
-.group-members-page
+.group-members-page.prepend-top-default
- if current_user && current_user.can?(:admin_group_member, @group)
.panel.panel-default
.panel-heading
@@ -20,7 +19,7 @@
group members
%small
(#{@members.total_count})
- .pull-right
+ .controls
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 66fe7e25871..486d1d8587a 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 90ade1e1680..b0805593fdc 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -4,17 +4,15 @@
- if current_user
= auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
-.project-issuable-filter
- .controls
- .pull-left
- - if current_user
- .hidden-xs.pull-left
- = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
- %i.fa.fa-rss
-
+.top-area
+ = render 'shared/issuable/nav', type: :issues
+ .nav-controls
+ - if current_user
+ = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
+ = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
- = render 'shared/issuable/filter', type: :issues
+= render 'shared/issuable/filter', type: :issues
.gray-content-block.second-block
Only issues from
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index f662f5a8c17..e1c9dd931ee 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,11 +1,12 @@
- page_title "Merge Requests"
- header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group))
-.project-issuable-filter
- .controls
+.top-area
+ = render 'shared/issuable/nav', type: :merge_requests
+ .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
- = render 'shared/issuable/filter', type: :merge_requests
+= render 'shared/issuable/filter', type: :merge_requests
.gray-content-block.second-block
Only merge requests from
diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml
deleted file mode 100644
index 9b85d83d6d8..00000000000
--- a/app/views/groups/milestones/_issue.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
- %span.milestone-row
- - project = issue.project
- %strong #{project.name} &middot;
- = link_to [project.namespace.becomes(Namespace), project, issue] do
- %span.cgray ##{issue.iid}
- = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
- .pull-right.assignee-icon
- - if issue.assignee
- = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_issues.html.haml b/app/views/groups/milestones/_issues.html.haml
deleted file mode 100644
index 9f350b772bd..00000000000
--- a/app/views/groups/milestones/_issues.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list issues-sortable-list" }
- - if issues
- - issues.each do |issue|
- = render 'issue', issue: issue
diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml
deleted file mode 100644
index e3aa4aad198..00000000000
--- a/app/views/groups/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
- %span.milestone-row
- - project = merge_request.project
- %strong #{project.name} &middot;
- = link_to [project.namespace.becomes(Namespace), project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
- .pull-right.assignee-icon
- - if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_merge_requests.html.haml b/app/views/groups/milestones/_merge_requests.html.haml
deleted file mode 100644
index 50057e2c636..00000000000
--- a/app/views/groups/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list" }
- - if merge_requests
- - merge_requests.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
index a20bf75bc39..4c4e0a26728 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -1,29 +1,5 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to issues_group_path(@group, milestone_title: milestone.title) do
- = pluralize milestone.issue_count, 'Issue'
- &middot;
- = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do
- = pluralize milestone.merge_requests_count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
- .row
- .col-sm-6
- %div
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name
- .col-sm-6
- - if can?(current_user, :admin_milestones, @group)
- - if milestone.closed?
- = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
- - else
- = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
+= render 'shared/milestones/milestone',
+ milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title),
+ issues_path: issues_group_path(@group, milestone_title: milestone.title),
+ merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title),
+ milestone: milestone
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index b221d3a89a4..ab307708b75 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,17 +1,15 @@
- page_title "Milestones"
- header_title group_title(@group, "Milestones", group_milestones_path(@group))
-.project-issuable-filter
- .controls
- - if can?(current_user, :admin_milestones, @group)
- .pull-right
- %span.pull-right.hidden-xs
- = link_to new_group_milestone_path(@group), class: "btn btn-new" do
- = icon('plus')
- New Milestone
-
+.top-area
= render 'shared/milestones_filter'
+ .nav-controls
+ - if can?(current_user, :admin_milestones, @group)
+ = link_to new_group_milestone_path(@group), class: "btn btn-new" do
+ = icon('plus')
+ New Milestone
+
.gray-content-block
Only milestones from
%strong #{@group.name}
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 3894a0ece74..a8e1ed77da9 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -8,18 +8,18 @@
This will create milestone in every selected project
%hr
-= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f|
+= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f|
.row
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.clearfix
.error-alert
.form-group
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index d063b257b5e..fb6f0da28f8 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,112 +1,4 @@
-- page_title @milestone.title, "Milestones"
= render "header_title"
-
-.detail-page-header
- .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- - if @milestone.closed?
- Closed
- - else
- Open
- %span.identifier
- Milestone #{@milestone.title}
- .pull-right
- - if can?(current_user, :admin_milestones, @group)
- - if @milestone.active?
- = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
- - else
- = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
-
-.detail-page-description.gray-content-block.second-block
- %h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
-
-- if @milestone.complete? && @milestone.active?
- .alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. You may close the milestone now.
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Project
- %th Open issues
- %th State
- %th Due date
- - @milestone.milestones.each do |milestone|
- %tr
- %td
- = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- %td
- = milestone.issues.opened.count
- %td
- - if milestone.closed?
- Closed
- - else
- Open
- %td
- = milestone.expires_at
-
-.context
- %p.lead
- Progress:
- #{@milestone.closed_items_count} closed
- &ndash;
- #{@milestone.open_items_count} open
- = milestone_progress_bar(@milestone)
-
-%ul.center-top-menu.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab' do
- Issues
- %span.badge= @milestone.issue_count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
- Merge Requests
- %span.badge= @milestone.merge_requests_count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @milestone.participants.count
-
-.tab-content
- .tab-pane.active#tab-issues
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All issues in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'issues', title: "Open", issues: @milestone.opened_issues
- .col-md-6
- = render 'issues', title: "Closed", issues: @milestone.closed_issues
-
- .tab-pane#tab-merge-requests
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All merge requests in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
- .col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
-
- .tab-pane#tab-participants
- .gray-content-block.middle-block
- .oneline
- All participants to this milestone
-
- %ul.bordered-list
- - @milestone.participants.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
+= render 'shared/milestones/top', milestone: @milestone, group: @group
+= render 'shared/milestones/summary', milestone: @milestone
+= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index f1d507a50c7..dd75766121e 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,14 +1,14 @@
- page_title "Projects"
- header_title group_title(@group, "Projects", projects_group_path(@group))
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
%strong= @group.name
projects:
- if can? current_user, :admin_group, @group
- .panel-head-actions
+ .controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
- %i.fa.fa-plus
+ = icon('plus')
New Project
%ul.well-list
- @projects.each do |project|
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index 7ea574434c3..c66b82bb484 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html"
xml.id group_url(@group)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events[0].updated_at.xmlschema if @events[0]
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index c2c7c581b3e..23a34ac36dd 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
- unless can?(current_user, :read_group, @group)
- @disable_search_panel = true
@@ -6,6 +8,12 @@
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.cover-block
+ .cover-controls
+ - if @group && can?(current_user, :admin_group, @group)
+ = link_to icon('pencil'), edit_group_path(@group), class: 'btn'
+ - if current_user
+ = link_to icon('rss'), group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn'
+
.avatar-holder
= link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s90"
@@ -20,32 +28,33 @@
= markdown(@group.description, pipeline: :description)
- if can?(current_user, :read_group, @group)
- %ul.center-top-menu.no-top
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @projects.present?
- %li
- = link_to "#projects", 'data-toggle' => 'tab' do
- Projects
-
- .tab-content
- .tab-pane.active#activity
- .gray-content-block.activity-filter-block
- - if current_user
- = render "events/event_last_push", event: @last_push
- .pull-right
- = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do
- %i.fa.fa-rss
-
- = render 'shared/event_filter'
-
- .content_list
- = spinner
-
- .tab-pane#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?
+ %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
- else
- %p
- This group does not have public projects
+ %p.nav-links.no-top
+ No projects to show
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index e8e331dd109..da3c3711cdd 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -22,6 +22,14 @@
%td.shortcut
.key ?
%td Show this dialog
+ %tr
+ %td.shortcut
+ - if browser.mac?
+ .key &#8984; shift p
+ - else
+ .key ctrl shift p
+
+ %td Toggle Markdown preview
%tbody
%tr
%th
@@ -40,6 +48,28 @@
%td.shortcut
.key enter
%td Open Selection
+ %tbody
+ %tr
+ %th
+ %th Finding Project File
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Move selection up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ %td Move selection down
+ %tr
+ %td.shortcut
+ .key enter
+ %td Open Selection
+ %tr
+ %td.shortcut
+ .key esc
+ %td Go back
.col-lg-4
%table.shortcut-mappings
@@ -135,6 +165,10 @@
.key s
%td
Go to snippets
+ %tr
+ %td.shortcut
+ .key t
+ %td Go to finding file
.col-lg-4
%table.shortcut-mappings
%tbody{ class: 'hidden-shortcut network', style: 'display:none' }
@@ -203,6 +237,10 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit issue
%tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
%tr
%th
@@ -219,3 +257,7 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit merge request
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index d9ffda884c8..d084559abc3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -19,6 +19,8 @@
%li
= link_to 'Buttons', '#buttons'
%li
+ = link_to 'Dropdowns', '#dropdowns'
+ %li
= link_to 'Panels', '#panels'
%li
= link_to 'Alerts', '#alerts'
@@ -31,64 +33,91 @@
%h2#blocks Blocks
- %h4
+ .lead
+ Content block separated with botton border
+ %code .content-block
+
+ .example
+ .content-block
+ %h4 Normal block inside content
+ = lorem
+
+ .content-block
+ %h4 Second block
+ = lorem
+
+ .lead
+ Gray content block with side padding using
%code .gray-content-block
- .gray-content-block.middle-block
- %h4 Normal block inside content
- = lorem
+ .example
+ .gray-content-block
+ %h4 Normal block inside content
+ = lorem
- .gray-content-block.second-block
- %h4 Second block
- = lorem
+ .gray-content-block.second-block
+ %h4 Second block
+ = lorem
- %h4
+ .lead
+ Cover block for profile page with avatar, name and description
%code .cover-block
- %br
- .cover-block
- .avatar-holder
- = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
- .cover-title
- John Smith
-
- .cover-desc
- = lorem
+ .example
+ .cover-block
+ .avatar-holder
+ = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
+ .cover-title
+ John Smith
- .cover-controls
- = link_to '#', class: 'btn btn-gray' do
- = icon('pencil')
- &nbsp;
- = link_to '#', class: 'btn btn-gray' do
- = icon('rss')
+ .cover-desc
+ = lorem
+
+ .cover-controls
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('pencil')
+ &nbsp;
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('rss')
%h2#lists Lists
- %h4
+ .lead
+ Simple list using
%code .content-list
- %ul.content-list
- %li
- One item
- %li
- One item
- %li
- One item
- %h4
- %code .well-list
- %ul.well-list
- %li
- One item
- %li
- One item
- %li
- One item
+ .example
+ %ul.content-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
- %h4
- %code .panel .well-list
+ .lead
+ List with avatar, title and description using
+ %code .content-list
+
+ .example
+ %ul.content-list
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
- .panel.panel-default
- .panel-heading Your list
+ .lead
+ List with hover effect
+ %code .well-list
+ .example
%ul.well-list
%li
One item
@@ -97,17 +126,18 @@
%li
One item
- %h4
- %code .bordered-list
- %ul.bordered-list
- %li
- One item
- %li
- One item
- %li
- One item
-
-
+ .lead
+ List inside panel
+ .example
+ .panel.panel-default
+ .panel-heading Your list
+ %ul.well-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
%h2#tables Tables
@@ -138,27 +168,34 @@
%h2#navs Navigation
- %h4
- %code .center-top-menu
- .example
- %ul.center-top-menu
- %li.active
- %a Open
- %li
- %a Closed
+ .lead
+ Holder for top page navigation. Includes navigation, search field, sorting and button
+ %code .top-area
- %h4
- %code .btn-group.btn-group-next
.example
- %div.btn-group.btn-group-next
- %a.btn.active Open
- %a.btn Closed
-
-
- %h4
- %code .nav.nav-tabs
+ .top-area
+ %ul.nav-links
+ %li.active
+ %a Open
+ %li
+ %a Closed
+ .nav-controls
+ = text_field_tag 'sample', nil, class: 'form-control'
+ .dropdown
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span Sort by name
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a Sort by date
+
+ = link_to 'New issue', '#', class: 'btn btn-new'
+
+ .lead
+ Only nav links without button and search
+ %code .nav-links
.example
- %ul.nav.nav-tabs
+ %ul.nav-links
%li.active
%a Open
%li
@@ -177,6 +214,227 @@
%button.btn.btn-danger{:type => "button"} Danger
%button.btn.btn-link{:type => "button"} Link
+ %h2#dropdowns Dropdowns
+
+ .example
+ .clearfix
+ .dropdown.inline.pull-left
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown.inline.pull-right
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-selectable
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li.divider
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown-footer
+ %strong Tip:
+ If an author is not a member of this project, you can still filter by his name while using the search field.
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown loading
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li.divider
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown-footer
+ %strong Tip:
+ If an author is not a member of this project, you can still filter by his name while using the search field.
+ .dropdown-loading
+ = icon('spinner spin')
+
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown user
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.dropdown-menu-user-link.is-active{href: "#"}
+ = link_to_member_avatar(current_user, size: 30)
+ %strong.dropdown-menu-user-full-name
+ = current_user.name
+ .dropdown-menu-user-username
+ = current_user.to_reference
+
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown page 2
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
+ .dropdown-page-one
+ .dropdown-title
+ %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ = icon('arrow-left')
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.dropdown-menu-user-link.is-active{href: "#"}
+ = link_to_member_avatar(current_user, size: 30)
+ %strong.dropdown-menu-user-full-name
+ = current_user.name
+ .dropdown-menu-user-username
+ = current_user.to_reference
+ .dropdown-page-two
+ .dropdown-title
+ %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ = icon('arrow-left')
+ %span Create label
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Name new label"}
+ .dropdown-content
+ %button.btn.btn-primary
+ Create
+
+ .example
+ %div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ .dropdown-loading
+ = icon('spinner spin')
+ :javascript
+ $('#js-project-dropdown').glDropdown({
+ data: function (term, callback) {
+ Api.projects(term, "last_activity_at", function (data) {
+ callback(data);
+ });
+ },
+ text: function (project) {
+ return project.name_with_namespace || project.name;
+ },
+ selectable: true,
+ fieldName: "author_id",
+ filterable: true,
+ search: {
+ fields: ['name_with_namespace']
+ },
+ id: function (data) {
+ return data.id;
+ },
+ isSelected: function (data) {
+ return data.id === 2;
+ }
+ })
+
+ .example
+ %div
+ = dropdown_tag("Projects", options: { title: "Go to project", filter: true, placeholder: "Filter projects" })
+
%h2#panels Panels
.row
@@ -221,43 +479,47 @@
%h2#forms Forms
- %h4
+ .lead
+ Horizontal form when label rendered inline with input
%code form.horizontal-form
- %form.form-horizontal
- .form-group
- %label.col-sm-2.control-label{:for => "inputEmail3"} Email
- .col-sm-10
- %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
- .form-group
- %label.col-sm-2.control-label{:for => "inputPassword3"} Password
- .col-sm-10
- %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- %label
- %input{:type => "checkbox"}/
- Remember me
- .form-group
- .col-sm-offset-2.col-sm-10
- %button.btn.btn-default{:type => "submit"} Sign in
-
- %h4
+ .example
+ %form.form-horizontal
+ .form-group
+ %label.col-sm-2.control-label{:for => "inputEmail3"} Email
+ .col-sm-10
+ %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
+ .form-group
+ %label.col-sm-2.control-label{:for => "inputPassword3"} Password
+ .col-sm-10
+ %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ %label
+ %input{:type => "checkbox"}/
+ Remember me
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %button.btn.btn-default{:type => "submit"} Sign in
+
+ .lead
+ Form when label rendered above input
%code form
- %form
- .form-group
- %label{:for => "exampleInputEmail1"} Email address
- %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
- .form-group
- %label{:for => "exampleInputPassword1"} Password
- %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
- .checkbox
- %label
- %input{:type => "checkbox"}/
- Remember me
- %button.btn.btn-default{:type => "submit"} Sign in
+ .example
+ %form
+ .form-group
+ %label{:for => "exampleInputEmail1"} Email address
+ %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
+ .form-group
+ %label{:for => "exampleInputPassword1"} Password
+ %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
+ .checkbox
+ %label
+ %input{:type => "checkbox"}/
+ Remember me
+ %button.btn.btn-default{:type => "submit"} Sign in
%h2#file File
%h4
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index 00c5f0b6f4e..c805914fc3f 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -5,5 +5,9 @@
-# num_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.next
- = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote
+- if current_page.last?
+ %li{ class: "next disabled" }
+ %span= raw(t 'views.pagination.next')
+- else
+ %li{ class: "next" }
+ = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index 2f645186921..a12c53bcfe7 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -10,13 +10,13 @@
%ul.pagination.clearfix
- unless current_page.first?
= first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages
- = prev_page_tag
+ = prev_page_tag
- each_page do |page|
- if page.left_outer? || page.right_outer? || page.inside_window?
= page_tag page
- elsif !page.was_truncated?
= gap_tag
+ = next_page_tag
- unless current_page.last?
- = next_page_tag
= last_page_tag unless num_pages < 5
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index f673abdb3ae..afb20455e0a 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -5,5 +5,9 @@
-# num_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{class: "prev" }
- = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote
+- if current_page.first?
+ %li{ class: "prev disabled" }
+ %span= raw(t 'views.pagination.previous')
+- else
+ %li{ class: "prev" }
+ = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote
diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml
index e7d477c225e..3a7e0929c16 100644
--- a/app/views/layouts/_broadcast.html.haml
+++ b/app/views/layouts/_broadcast.html.haml
@@ -1,4 +1 @@
-- if broadcast_message.present?
- .broadcast-message{ style: broadcast_styling(broadcast_message) }
- %i.fa.fa-bullhorn
- = broadcast_message.message
+= broadcast_message
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index dd133ee8b56..79cdbac1f37 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,10 +1,13 @@
+- page_description brand_title unless page_description
+
+- site_name = "GitLab"
%head{prefix: "og: http://ogp.me/ns#"}
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
-# Open Graph - http://ogp.me/
%meta{property: 'og:type', content: "object"}
- %meta{property: 'og:site_name', content: "GitLab"}
+ %meta{property: 'og:site_name', content: site_name}
%meta{property: 'og:title', content: page_title}
%meta{property: 'og:description', content: page_description}
%meta{property: 'og:image', content: page_image}
@@ -17,7 +20,7 @@
%meta{property: 'twitter:image', content: page_image}
= page_card_meta_tags
- %title= page_title('GitLab')
+ %title= page_title(site_name)
%meta{name: "description", content: page_description}
= favicon_link_tag 'favicon.ico'
@@ -41,6 +44,7 @@
= favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
= favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120'
= favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
+ %link{rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)'}
-# Windows 8 pinned site tile
%meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')}
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 035fe0056d3..96b38485425 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,4 +1,6 @@
- project = @target_project || @project
-:javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
- GitLab.GfmAutoComplete.setup();
+
+- if @noteable
+ :javascript
+ GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
+ GitLab.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index ec7cd79bc54..c799e9c588d 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,9 +1,10 @@
-.page-with-sidebar{ class: page_sidebar_class }
+.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
= render "layouts/broadcast"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
+ %a#logo
= brand_header_logo
+ = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
.gitlab-text-container
%h3 GitLab
@@ -24,7 +25,7 @@
.content-wrapper
= render "layouts/flash"
= yield :flash_message
- %div{ class: container_class }
+ %div{ class: (container_class unless @no_container) }
.content
.clearfix
= yield
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index a44f5762a6b..54af2c3063c 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,6 +1,6 @@
.search
= form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f|
- = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control", spellcheck: false
+ = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1"
= hidden_field_tag :group_id, @group.try(:id)
- if @project && @project.persisted?
= hidden_field_tag :project_id, @project.id
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 678ed3c2c1f..babfb032236 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -5,11 +5,7 @@
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
= yield :scripts_body_top
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
+ = render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar
= yield :scripts_body
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
index 7e90af21b21..a13241bebee 100644
--- a/app/views/layouts/ci/_page.html.haml
+++ b/app/views/layouts/ci/_page.html.haml
@@ -2,8 +2,9 @@
= render "layouts/broadcast"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
+ %a#logo
= brand_header_logo
+ = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
.gitlab-text-container
%h3 GitLab
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 31888c5580e..2e483b7148d 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,5 +1,6 @@
-- page_title @group.name
-- header_title group_title(@group) unless header_title
-- sidebar "group" unless sidebar
+- page_title @group.name
+- page_description @group.description unless page_description
+- header_title group_title(@group) unless header_title
+- sidebar "group" unless sidebar
= render template: "layouts/application"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 3892ef8eefa..77d01a7736c 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -13,27 +13,39 @@
%li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- - if session[:impersonator_id]
- %li.impersonation
- = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('user-secret fw')
- - if current_user.is_admin?
+ - if current_user
+ - if session[:impersonator_id]
+ %li.impersonation
+ = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = icon('user-secret fw')
+ - if current_user.is_admin?
+ %li
+ = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('wrench fw')
%li
- = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
- - if current_user.can_create_project?
+ = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ %span.badge.todos-pending-count
+ = todos_pending_count
+ - if current_user.can_create_project?
+ %li
+ = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('plus fw')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('tachometer fw')
%li
- = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('plus fw')
- - if Gitlab::Sherlock.enabled?
- %li
- = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('tachometer fw')
- %li
- = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('sign-out')
+ = 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'
+
%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)}";
diff --git a/app/views/layouts/header/_public.html.haml b/app/views/layouts/header/_public.html.haml
deleted file mode 100644
index a6a26518a0e..00000000000
--- a/app/views/layouts/header/_public.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
- .header-content
- - unless current_controller?('sessions')
- .pull-right
- = 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'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index c60ac5eefac..280a1b93729 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,6 +1,6 @@
%ul.nav.nav-sidebar
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: "Stats" do
+ = link_to admin_root_path, title: 'Overview' do
= icon('dashboard fw')
%span
Overview
@@ -25,17 +25,17 @@
%span
Deploy Keys
= nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path do
+ = link_to admin_runners_path, title: 'Runners' do
= icon('cog fw')
%span
Runners
- %span.count= Ci::Runner.count(:all)
+ %span.count= number_with_delimiter(Ci::Runner.count(:all))
= nav_link path: 'builds#index' do
- = link_to admin_builds_path do
+ = link_to admin_builds_path, title: 'Builds' do
= icon('link fw')
%span
Builds
- %span.count= Ci::Build.count(:all)
+ %span.count= number_with_delimiter(Ci::Build.count(:all))
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do
= icon('file-text fw')
@@ -56,6 +56,11 @@
= icon('cog fw')
%span
Background Jobs
+ = nav_link(controller: :appearances) do
+ = link_to admin_appearances_path, title: 'Appearances' do
+ = icon('image')
+ %span
+ Appearance
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
@@ -80,7 +85,15 @@
= icon('exclamation-circle fw')
%span
Abuse Reports
- %span.count= AbuseReport.count(:all)
+ %span.count= number_with_delimiter(AbuseReport.count(:all))
+
+ - if askimet_enabled?
+ = nav_link(controller: :spam_logs) do
+ = link_to admin_spam_logs_path, title: "Spam Logs" do
+ = icon('exclamation-triangle fw')
+ %span
+ Spam Logs
+ %span.count= number_with_delimiter(SpamLog.count(:all))
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= link_to admin_application_settings_path, title: 'Settings' do
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index da698831300..db0cf393922 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -4,6 +4,12 @@
= icon('home fw')
%span
Projects
+ = nav_link(controller: :todos) do
+ = link_to dashboard_todos_path, title: 'Todos' do
+ = icon('bell fw')
+ %span
+ Todos
+ %span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do
= icon('dashboard fw')
@@ -24,13 +30,13 @@
= icon('exclamation-circle fw')
%span
Issues
- %span.count= current_user.assigned_issues.opened.count
- = nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- = icon('tasks fw')
- %span
- Merge Requests
- %span.count= current_user.assigned_merge_requests.opened.count
+ %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+ = nav_link(path: 'dashboard#merge_requests') do
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
+ = icon('tasks fw')
+ %span
+ Merge Requests
+ %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 68da8d5de2a..59411ae1da1 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -9,10 +9,15 @@
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
- = icon('dashboard fw')
+ = 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
@@ -25,14 +30,14 @@
%span
Issues
- if current_user
- %span.count= Issue.opened.of_group(@group).count
+ %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= MergeRequest.opened.of_group(@group).count
+ %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')
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 64b30783c05..3b9d31a6fc5 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -17,7 +17,7 @@
= icon('gear fw')
%span
Account
- = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do
+ = nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
= icon('cloud fw')
%span
@@ -27,7 +27,7 @@
= icon('envelope-o fw')
%span
Emails
- %span.count= current_user.emails.count + 1
+ %span.count= number_with_delimiter(current_user.emails.count + 1)
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
@@ -45,7 +45,7 @@
= icon('key fw')
%span
SSH Keys
- %span.count= current_user.keys.count
+ %span.count= number_with_delimiter(current_user.keys.count)
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
-# TODO (rspeicher): Better icon?
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index c0d62028639..86b46e8c75e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -16,7 +16,7 @@
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Project
= nav_link(path: 'projects#activity') do
@@ -25,7 +25,7 @@
%span
Activity
- if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do
= icon('files-o fw')
%span
@@ -44,7 +44,7 @@
= icon('cubes fw')
%span
Builds
- %span.count.builds_counter= @project.builds.running_or_pending.count(:all)
+ %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all))
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
@@ -67,7 +67,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.count.issue_counter= @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
@@ -75,7 +75,7 @@
= icon('tasks fw')
%span
Merge Requests
- %span.count.merge_counter= @project.merge_requests.opened.count
+ %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- if project_nav_tab? :settings
= nav_link(controller: [:project_members, :teams]) do
@@ -98,6 +98,13 @@
%span
Wiki
+ - if project_nav_tab? :forks
+ = nav_link(controller: :forks, action: :index) do
+ = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do
+ = icon('code-fork fw')
+ %span
+ Forks
+
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do
@@ -117,4 +124,3 @@
%li.hidden
= link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
Network
-
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 970da78a5c9..dc3050f02e5 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -13,16 +13,22 @@
= icon('pencil-square-o fw')
%span
Project Settings
+ - if @project.allowed_to_share_with_group?
+ = nav_link(controller: :group_links) do
+ = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
+ = icon('share-square-o fw')
+ %span
+ Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
%span
Deploy Keys
= nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do
+ = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
= icon('link fw')
%span
- Web Hooks
+ Webhooks
= nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
= icon('cogs fw')
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 3ca4c340406..37b4d562966 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -42,8 +42,15 @@
- else
#{link_to "View it on GitLab", @target_url}.
%br
- -# Don't link the host is the line below, one link in the email is easier to quickly click than two.
+ -# Don't link the host in the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
- If you'd like to receive fewer emails, you can adjust your notification settings.
+ If you'd like to receive fewer emails, you can
+ - if @labels_url
+ adjust your #{link_to 'label subscriptions', @labels_url}.
+ - else
+ - if @sent_notification && @sent_notification.unsubscribable?
+ = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+ from this thread or
+ adjust your notification settings.
- = email_action @target_url \ No newline at end of file
+ = email_action @target_url
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index abf73bcc709..ab527e8e438 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,6 +1,7 @@
-- page_title @project.name_with_namespace
-- header_title project_title(@project) unless header_title
-- sidebar "project" unless sidebar
+- page_title @project.name_with_namespace
+- page_description @project.description unless page_description
+- header_title project_title(@project) unless header_title
+- sidebar "project" unless sidebar
- content_for :scripts_body_top do
- project = @target_project || @project
diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml
index 00cb4aa24cc..12ded41fbf2 100644
--- a/app/views/notify/_note_message.html.haml
+++ b/app/views/notify/_note_message.html.haml
@@ -1,2 +1,5 @@
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @note.author_name, user_url(@note.author)} wrote:
%div
= markdown(@note.note, pipeline: :email)
diff --git a/app/views/notify/_reassigned_issuable_email.text.erb b/app/views/notify/_reassigned_issuable_email.text.erb
index 855d37429d9..daf20a226dd 100644
--- a/app/views/notify/_reassigned_issuable_email.text.erb
+++ b/app/views/notify/_reassigned_issuable_email.text.erb
@@ -1,6 +1,6 @@
Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %>
-<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, {only_path: false}]) %>
+<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %>
diff --git a/app/views/notify/_relabeled_issuable_email.html.haml b/app/views/notify/_relabeled_issuable_email.html.haml
new file mode 100644
index 00000000000..80a0de255be
--- /dev/null
+++ b/app/views/notify/_relabeled_issuable_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ #{'Label'.pluralize(@label_names.size)} added:
+ %em= @label_names.to_sentence
diff --git a/app/views/notify/_relabeled_issuable_email.text.erb b/app/views/notify/_relabeled_issuable_email.text.erb
new file mode 100644
index 00000000000..6a83d79fd61
--- /dev/null
+++ b/app/views/notify/_relabeled_issuable_email.text.erb
@@ -0,0 +1,3 @@
+<%= 'Label'.pluralize(@label_names.size) %> added: <%= @label_names.to_sentence %>
+
+<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
index f4e9749e5c7..81d65037312 100644
--- a/app/views/notify/build_fail_email.html.haml
+++ b/app/views/notify/build_fail_email.html.haml
@@ -1,9 +1,10 @@
- content_for :header do
%h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
GitLab (build failed)
+
%h3
Project:
- = link_to ci_project_url(@project) do
+ = link_to namespace_project_url(@project.namespace, @project) do
= @project.name
%p
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
index 8b004d34cca..5d247eb4cf2 100644
--- a/app/views/notify/build_success_email.html.haml
+++ b/app/views/notify/build_success_email.html.haml
@@ -4,7 +4,7 @@
%h3
Project:
- = link_to ci_project_url(@project) do
+ = link_to namespace_project_url(@project.namespace, @project) do
= @project.name
%p
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index d3b799fca23..ad3ab2525bb 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,3 +1,6 @@
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @issue.author_name, user_url(@issue.author)} wrote:
-if @issue.description
= markdown(@issue.description, pipeline: :email)
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 90ebdfc3fe2..23423e7d981 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -1,3 +1,6 @@
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote:
%p.details
!= merge_path_description(@merge_request, '&rarr;')
diff --git a/app/views/notify/relabeled_issue_email.html.haml b/app/views/notify/relabeled_issue_email.html.haml
new file mode 100644
index 00000000000..b17b16e1814
--- /dev/null
+++ b/app/views/notify/relabeled_issue_email.html.haml
@@ -0,0 +1 @@
+= render 'relabeled_issuable_email', issuable: @issue
diff --git a/app/views/notify/relabeled_issue_email.text.erb b/app/views/notify/relabeled_issue_email.text.erb
new file mode 100644
index 00000000000..eeced97f601
--- /dev/null
+++ b/app/views/notify/relabeled_issue_email.text.erb
@@ -0,0 +1 @@
+<%= render 'relabeled_issuable_email', issuable: @issue %>
diff --git a/app/views/notify/relabeled_merge_request_email.html.haml b/app/views/notify/relabeled_merge_request_email.html.haml
new file mode 100644
index 00000000000..9eaa9afa5b1
--- /dev/null
+++ b/app/views/notify/relabeled_merge_request_email.html.haml
@@ -0,0 +1 @@
+= render 'relabeled_issuable_email', issuable: @merge_request
diff --git a/app/views/notify/relabeled_merge_request_email.text.erb b/app/views/notify/relabeled_merge_request_email.text.erb
new file mode 100644
index 00000000000..87bc80ead32
--- /dev/null
+++ b/app/views/notify/relabeled_merge_request_email.text.erb
@@ -0,0 +1 @@
+<%= render 'relabeled_issuable_email', issuable: @merge_request %>
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index 4361f67a74d..f2e405b14fd 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -17,8 +17,8 @@
%strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))}
%div
%span by #{commit.author_name}
- %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
- %pre.commit-message
+ %i at #{commit.committed_date.to_s(:iso8601)}
+ %pre.commit-message
= commit.safe_message
%h4 #{pluralize @message.diffs_count, "changed file"}:
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index aa0e263b6df..53869e36b28 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -8,7 +8,7 @@
\
= @message.reverse_compare? ? "Deleted commits:" : "Commits:"
- @message.commits.each do |commit|
- #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
+ #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.to_s(:iso8601)}
#{commit.safe_message}
\- - - - -
\
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index 58af79716a7..879fc170f92 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -1,17 +1,15 @@
-.table-holder
- %table.table#audits
- %thead
- %tr
- %th Action
- %th When
+%h5.prepend-top-0
+ History of authentications
+
+%ul.well-list
+ - events.each do |event|
+ %li
+ %span.description
+ = audit_icon(event.details[:with], class: "append-right-5")
+ Signed in with
+ = event.details[:with]
+ authentication
+ %span.pull-right
+ #{time_ago_in_words event.created_at} ago
- %tbody
- - events.each do |event|
- %tr
- %td
- %span
- Signed in with
- %b= event.details[:with]
- authentication
- %td #{time_ago_in_words event.created_at} ago
= paginate events, theme: "gitlab"
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 17e47c622ce..6efd119f260 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,120 +1,117 @@
- page_title "Account"
- header_title page_title, profile_account_path
-- @blank_container = true
- if current_user.ldap_user?
.alert.alert-info
Some options are unavailable for LDAP accounts
-.account-page
- .panel.panel-default.update-token
- .panel-heading
- Reset Private token
- .panel-body
- = form_for @user, url: reset_private_token_profile_path, method: :put do |f|
- .data
- %p
- Your private token is used to access application resources without authentication.
- %br
- It can be used for atom feeds or the API.
- %span.cred
- Keep it secret!
-
- %p.cgray
- - if current_user.private_token
- = text_field_tag "token", current_user.private_token, class: "form-control"
- - else
- %span You don`t have one yet. Click generate to fix it.
-
- .form-actions
- - if current_user.private_token
- = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
- - else
- = f.submit 'Generate', class: "btn btn-default"
-
- - unless current_user.ldap_user?
- .panel.panel-default
- .panel-heading
- Two-factor Authentication
- .panel-body
- - if current_user.two_factor_enabled?
- .pull-right
- = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm',
- data: { confirm: 'Are you sure?' }
- %p.text-success
- %strong
- Two-factor Authentication is enabled
- %p
- If you lose your recovery codes you can
- %strong
- = succeed ',' do
- = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' }
- invalidating all previous codes.
-
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Private Token
+ %p
+ Your private token is used to access application resources without authentication.
+ .col-lg-9
+ = form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f|
+ %p.cgray
+ - if current_user.private_token
+ = label_tag "token", "Private token", class: "label-light"
+ = text_field_tag "token", current_user.private_token, class: "form-control"
- else
- %p
- Increase your account's security by enabling two-factor authentication (2FA).
- %p
- Each time you log in you’ll be required to provide your username and
- password as usual, plus a randomly-generated code from your phone.
-
- .form-actions
- = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
-
- - if button_based_providers.any?
- .panel.panel-default
- .panel-heading
+ %span You don`t have one yet. Click generate to fix it.
+ %p.help-block
+ It can be used for atom feeds or the API. Keep it secret!
+ .prepend-top-default
+ - if current_user.private_token
+ = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
+ - else
+ = f.submit 'Generate', class: "btn btn-default"
+%hr
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Two-factor Authentication
+ %p
+ Increase your account's security by enabling two-factor authentication (2FA).
+ .col-lg-9
+ %p
+ Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
+ - if !current_user.two_factor_enabled?
+ %p
+ Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
+ More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
+ .append-bottom-10
+ = link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
+ - else
+ = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger',
+ data: { confirm: 'Are you sure?' }
+%hr
+- if button_based_providers.any?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Social sign-in
+ %p
+ Activate signin with one of the following services
+ .col-lg-9
+ %label.label-light
Connected Accounts
- .panel-body
- .oauth-buttons.append-bottom-10
- %p Click on icon to activate signin with one of the following services
- - button_based_providers.each do |provider|
- .btn-group
- = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true"
-
- - if auth_active?(provider)
- = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do
- = icon('close')
-
- - if current_user.can_change_username?
- .panel.panel-warning.update-username
- .panel-heading
- Change Username
- .panel-body
- = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f|
- %p
- Changing your username will change path to all personal projects!
- %div
- .input-group
- .input-group-addon
- = "#{root_url}u/"
- = f.text_field :username, required: true, class: 'form-control'
- &nbsp;
- .loading-gif.hide
- %p
- = icon('spinner spin')
- Saving new username
- .form-actions
- = f.submit 'Save username', class: "btn btn-warning"
+ %p Click on icon to activate signin with one of the following services
+ - button_based_providers.each do |provider|
+ .provider-btn-group
+ .provider-btn-image
+ = provider_image_tag(provider)
+ - if auth_active?(provider)
+ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
+ Disconnect
+ - else
+ = link_to user_omniauth_authorize_path(provider), method: :post, class: "provider-btn #{'not-active' if !auth_active?(provider)}", "data-no-turbolink" => "true" do
+ Connect
+ %hr
+- if current_user.can_change_username?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0.change-username-title
+ Change username
+ %p
+ Changing your username will change path to all personal projects!
+ .col-lg-9
+ = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f|
+ .form-group
+ = f.label :username, "Path", class: "label-light"
+ .input-group
+ .input-group-addon
+ = "#{root_url}u/"
+ = f.text_field :username, required: true, class: 'form-control'
+ .help-block
+ Current path:
+ = "#{root_url}u/#{current_user.username}"
+ .prepend-top-default
+ = f.button class: "btn btn-warning", type: "submit" do
+ = icon "spinner spin", class: "hidden loading-username"
+ Update username
+ %hr
- - if signup_enabled?
- .panel.panel-danger.remove-account
- .panel-heading
+- if signup_enabled?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0.remove-account-title
Remove account
- .panel-body
- - if @user.can_be_removed?
- %p Deleting an account has the following effects:
- %ul
- %li All user content like authored issues, snippets, comments will be removed
- - rp = current_user.personal_projects.count
- - unless rp.zero?
- %li #{pluralize rp, 'personal project'} will be removed and cannot be restored
- .form-actions
- = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- - else
- - if @user.solo_owned_groups.present?
- %p
- Your account is currently an owner in these groups:
- %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
- %p
- You must transfer ownership or delete these groups before you can delete your account.
+ .col-lg-9
+ - if @user.can_be_removed?
+ %p
+ Deleting an account has the following effects:
+ %ul
+ %li All user content like authored issues, snippets, comments will be removed
+ - rp = current_user.personal_projects.count
+ - unless rp.zero?
+ %li #{pluralize rp, 'personal project'} will be removed and cannot be restored
+ = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
+ - else
+ - if @user.solo_owned_groups.present?
+ %p
+ Your account is currently an owner in these groups:
+ %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
+ %p
+ You must transfer ownership or delete these groups before you can delete your account.
+.append-bottom-default
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
deleted file mode 100644
index 0436c2213da..00000000000
--- a/app/views/profiles/applications.html.haml
+++ /dev/null
@@ -1,70 +0,0 @@
-- page_title "Applications"
-- header_title page_title, applications_profile_path
-
-.gray-content-block.top-block
- - if user_oauth_applications?
- Manage applications that can use GitLab as an OAuth provider,
- and applications that you've authorized to use your account.
- - else
- Manage applications that you've authorized to use your account.
-
-- if user_oauth_applications?
- .oauth-applications
- %h3
- Your applications
- .pull-right
- = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
- - if @applications.any?
- .table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th Clients
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td
- - application.redirect_uri.split.each do |uri|
- %div= uri
- %td= application.access_tokens.count
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
- %td= render 'doorkeeper/applications/delete_form', application: application
-
-.oauth-authorized-applications.prepend-top-20
- - if user_oauth_applications?
- %h3
- Authorized applications
-
- - if @authorized_tokens.any?
- .table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Authorized At
- %th Scope
- %th
- %tbody
- - @authorized_apps.each do |app|
- - token = app.authorized_tokens.order('created_at desc').first
- %tr{:id => "application_#{app.id}"}
- %td= app.name
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- - @authorized_anonymous_tokens.each do |token|
- %tr
- %td
- Anonymous
- %div.help-block
- %em Authorization was granted by entering your username and password in the application.
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- - else
- %p.light You don't have any authorized applications
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index 8fdba45b193..f630c03e5f6 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,8 +1,11 @@
- page_title "Audit Log"
- header_title page_title, audit_log_profile_path
-.gray-content-block.top-block
- History of authentications
-
-.prepend-top-default
-= render 'event_table', events: @events
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h3.prepend-top-0
+ = page_title
+ %p
+ This is a security log of important events involving your account.
+ .col-lg-9
+ = render 'event_table', events: @events
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 1d140347a5f..3f328f96cea 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,52 +1,49 @@
- page_title "Emails"
- header_title page_title, profile_emails_path
-.gray-content-block.top-block
- Control emails linked to your account
-
-%ul.prepend-top-default
- %li
- Your
- %b Primary Email
- will be used for avatar detection and web based operations, such as edits and merges.
- %li
- Your
- %b Notification Email
- will be used for account notifications.
- %li
- Your
- %b Public Email
- will be displayed on your public profile.
- %li
- All email addresses will be used to identify your commits.
-
-.panel.panel-default
- .panel-heading
- Emails (#{@emails.count + 1})
- %ul.well-list#emails-table
- %li
- %strong= @primary
- %span.label.label-success Primary Email
- - if @primary === current_user.public_email
- %span.label.label-info Public Email
- - if @primary === current_user.notification_email
- %span.label.label-info Notification Email
- - @emails.each do |email|
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ Control emails linked to your account
+ .col-lg-9
+ %h4.prepend-top-0
+ Add email address
+ = form_for 'email', url: profile_emails_path do |f|
+ .form-group
+ = f.label :email, class: 'label-light'
+ = f.text_field :email, class: 'form-control'
+ .prepend-top-default
+ = f.submit 'Add email address', class: 'btn btn-create'
+ %hr
+ %h4.prepend-top-0
+ Linked emails (#{@emails.count + 1})
+ .account-well.append-bottom-default
+ %ul
+ %li
+ Your Primary Email will be used for avatar detection and web based operations, such as edits and merges.
+ %li
+ Your Notification Email will be used for account notifications.
+ %li
+ Your Public Email will be displayed on your public profile.
+ %li
+ All email addresses will be used to identify your commits.
+ %ul.well-list
%li
- %strong= email.email
- - if email.email === current_user.public_email
- %span.label.label-info Public Email
- - if email.email === current_user.notification_email
- %span.label.label-info Notification Email
- %span.cgray
- added #{time_ago_with_tooltip(email.created_at)}
- = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right'
-
-%h4 Add email address
-= form_for 'email', url: profile_emails_path, html: { class: 'form-horizontal' } do |f|
- .form-group
- = f.label :email, class: 'control-label'
- .col-sm-10
- = f.text_field :email, class: 'form-control'
- .form-actions
- = f.submit 'Add email address', class: 'btn btn-create'
+ = @primary
+ %span.pull-right
+ %span.label.label-success Primary Email
+ - if @primary === current_user.public_email
+ %span.label.label-info Public Email
+ - if @primary === current_user.notification_email
+ %span.label.label-info Notification Email
+ - @emails.each do |email|
+ %li
+ = email.email
+ %span.pull-right
+ - if email.email === current_user.public_email
+ %span.label.label-info Public Email
+ - if email.email === current_user.notification_email
+ %span.label.label-info Notification Email
+ = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right'
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 2a8800de60e..4d78215ed3c 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -1,5 +1,5 @@
%div
- = form_for [:profile, @key], html: { class: 'form-horizontal js-requires-input' } do |f|
+ = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
- if @key.errors.any?
.alert.alert-danger
%ul
@@ -7,13 +7,11 @@
%li= msg
.form-group
- = f.label :key, class: 'control-label'
- .col-sm-10
- = f.text_area :key, class: "form-control", rows: 8, autofocus: true, required: true
+ = f.label :key, class: 'label-light'
+ = f.text_area :key, class: "form-control", rows: 8, required: true
.form-group
- = f.label :title, class: 'control-label'
- .col-sm-10= f.text_field :title, class: "form-control", required: true
+ = f.label :title, class: 'label-light'
+ = f.text_field :title, class: "form-control", required: true
- .form-actions
+ .prepend-top-default
= f.submit 'Add key', class: "btn btn-create"
- = link_to "Cancel", profile_keys_path, class: "btn btn-cancel"
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 9bbccbc45ea..25e9e8ff008 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -1,11 +1,14 @@
-%tr
- %td
- = link_to path_to_key(key, is_admin) do
- %strong= key.title
- %td
- %code.key-fingerprint= key.fingerprint
- %td
- %span.cgray
- added #{time_ago_with_tooltip(key.created_at)}
- %td
- = link_to 'Remove', path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
+%li.key-list-item
+ .pull-left.append-right-10
+ = icon 'key', class: "key-icon hidden-xs"
+ .key-list-item-info
+ = link_to path_to_key(key, is_admin), class: "title" do
+ = key.title
+ .description
+ = key.fingerprint
+ .pull-right
+ %span.key-created-at
+ created #{time_ago_with_tooltip(key.created_at)} ago
+ = 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/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 0ca8bd95157..dd7615400dc 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -1,5 +1,5 @@
- is_admin = defined?(admin) ? true : false
-.row
+.row.prepend-top-default
.col-md-4
.panel.panel-default
.panel-heading
@@ -10,7 +10,7 @@
%strong= @key.title
%li
%span.light Created on:
- %strong= @key.created_at.stamp("Aug 21, 2011")
+ %strong= @key.created_at.to_s(:medium)
.col-md-8
%p
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index 8c9d546af4c..296cafa6e31 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -1,19 +1,11 @@
-- is_admin = defined?(admin) ? true : false
+- is_admin = local_assigns.fetch(:admin, false)
+
- if @keys.any?
- .table-holder
- %table.table
- %thead.panel-heading
- %tr
- %th Title
- %th Fingerprint
- %th Added at
- %th
- %tbody
- - @keys.each do |key|
- = render 'profiles/keys/key', key: key, is_admin: is_admin
+ %ul.well-list
+ = render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin }
- else
- .nothing-here-block
+ %p.profile-settings-message.text-center
- if is_admin
- User has no ssh keys
+ There are no SSH keys associated with this account.
- else
There are no SSH keys with access to your account.
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 17a4195030e..e0f8c9a5733 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,14 +1,21 @@
- page_title "SSH Keys"
- header_title page_title, profile_keys_path
-.gray-content-block.top-block
- .pull-right
- = link_to new_profile_key_path, class: "btn btn-new" do
- = icon('plus')
- Add SSH Key
- .oneline
- Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh", "README")
-
-.prepend-top-default
-= render 'key_table'
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ SSH keys allow you to establish a secure connection between your computer and GitLab.
+ .col-lg-9
+ %h5.prepend-top-0
+ Add an SSH key
+ %p.profile-settings-content
+ Before you can add an SSH key you need to
+ = link_to "generate it.", help_page_path("ssh", "README")
+ = render 'form'
+ %hr
+ %h5
+ Your SSH keys (#{@keys.count})
+ %div.append-bottom-default
+ = render 'key_table'
diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml
deleted file mode 100644
index 13a18269d11..00000000000
--- a/app/views/profiles/keys/new.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- page_title "Add SSH Keys"
-%h3.page-title Add an SSH Key
-%p.light
- Paste your public key here. Read more about how to generate a key on #{link_to "the SSH help page", help_page_path("ssh", "README")}.
-%hr
-= render 'form'
-
-:javascript
- $('#key_key').on('focusout', function(){
- var title = $('#key_title'),
- val = $('#key_key').val(),
- comment = val.match(/^\S+ \S+ (.+)\n?$/);
-
- if( comment && comment.length > 1 && title.val() == '' ){
- $('#key_title').val( comment[1] ).change();
- }
- });
diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml
index 742c5c4b68d..d0d044136f6 100644
--- a/app/views/profiles/notifications/_settings.html.haml
+++ b/app/views/profiles/notifications/_settings.html.haml
@@ -1,5 +1,5 @@
-%li
- %span.notification.fa.fa-holder
+%li.notification-list-item
+ %span.notification.fa.fa-holder.append-right-5
- if notification.global?
= notification_icon(@notification)
- else
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 0bcadc965fa..de80abd7f4d 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,11 +1,7 @@
- page_title "Notifications"
- header_title page_title, profile_notifications_path
-.gray-content-block.top-block
- These are your global notification settings.
-
-.prepend-top-default
-= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f|
+= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
-if @user.errors.any?
%div.alert.alert-danger
%ul
@@ -13,65 +9,66 @@
%li= msg
= hidden_field_tag :notification_type, 'global'
+ .row
+ .col-lg-3.profile-settings-sidebar
+ %h4
+ = page_title
+ %p
+ You can specify notification level per group or per project.
+ %p
+ By default, all projects and groups will use the global notifications setting.
+ .col-lg-9
+ %h5
+ Global notification settings
+ .form-group
+ = f.label :notification_email, class: "label-light"
+ = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
+ .form-group
+ = f.label :notification_level, class: 'label-light'
+ .radio
+ = f.label :notification_level, value: Notification::N_DISABLED do
+ = f.radio_button :notification_level, Notification::N_DISABLED
+ .level-title
+ Disabled
+ %p You will not get any notifications via email
- .form-group
- = f.label :notification_email, class: "control-label"
- .col-sm-10
- = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "form-control"
-
- .form-group
- = f.label :notification_level, class: 'control-label'
- .col-sm-10
- .radio
- = f.label :notification_level, value: Notification::N_DISABLED do
- = f.radio_button :notification_level, Notification::N_DISABLED
- .level-title
- Disabled
- %p You will not get any notifications via email
-
- .radio
- = f.label :notification_level, value: Notification::N_MENTION do
- = f.radio_button :notification_level, Notification::N_MENTION
- .level-title
- On Mention
- %p You will receive notifications only for comments in which you were @mentioned
-
- .radio
- = f.label :notification_level, value: Notification::N_PARTICIPATING do
- = f.radio_button :notification_level, Notification::N_PARTICIPATING
- .level-title
- Participating
- %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
-
- .radio
- = f.label :notification_level, value: Notification::N_WATCH do
- = f.radio_button :notification_level, Notification::N_WATCH
- .level-title
- Watch
- %p You will receive notifications for any activity
+ .radio
+ = f.label :notification_level, value: Notification::N_MENTION do
+ = f.radio_button :notification_level, Notification::N_MENTION
+ .level-title
+ On Mention
+ %p You will receive notifications only for comments in which you were @mentioned
- .gray-content-block
- = f.submit 'Save changes', class: "btn btn-create"
+ .radio
+ = f.label :notification_level, value: Notification::N_PARTICIPATING do
+ = f.radio_button :notification_level, Notification::N_PARTICIPATING
+ .level-title
+ Participating
+ %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
-.row.all-notifications.prepend-top-default
- .col-md-6
- %p
- You can also specify notification level per group or per project.
- %br
- By default, all projects and groups will use the notification level set above.
- %h4 Groups:
- %ul.bordered-list
- - @group_members.each do |group_member|
- - notification = Notification.new(group_member)
- = render 'settings', type: 'group', membership: group_member, notification: notification
+ .radio
+ = f.label :notification_level, value: Notification::N_WATCH do
+ = f.radio_button :notification_level, Notification::N_WATCH
+ .level-title
+ Watch
+ %p You will receive notifications for any activity
- .col-md-6
- %p
- To specify the notification level per project of a group you belong to,
- %br
- you need to be a member of the project itself, not only its group.
- %h4 Projects:
- %ul.bordered-list
- - @project_members.each do |project_member|
- - notification = Notification.new(project_member)
- = render 'settings', type: 'project', membership: project_member, notification: notification
+ .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
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index fab7c45c9b2..afd4f996b62 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,20 +1,18 @@
- page_title "Password"
- header_title page_title, edit_profile_password_path
-.gray-content-block.top-block
- - if @user.password_automatically_set?
- Set your password.
- - else
- Change your password or recover your current one.
-
-.update-password.prepend-top-default
- = form_for @user, url: profile_password_path, method: :put, html: { class: 'form-horizontal' } do |f|
- %div
- %p.slead
- - unless @user.password_automatically_set?
- You must provide current password in order to change it.
- %br
- After a successful password update, you will be redirected to the login page where you can log in with your new password.
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ After a successful password update, you will be redirected to the login page where you can log in with your new password.
+ .col-lg-9
+ %h5.prepend-top-0
+ Change your password
+ - unless @user.password_automatically_set?
+ or recover your current one
+ = form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f|
-if @user.errors.any?
.alert.alert-danger
%ul
@@ -22,19 +20,16 @@
%li= msg
- unless @user.password_automatically_set?
.form-group
- = f.label :current_password, class: 'control-label'
- .col-sm-10
- = f.password_field :current_password, required: true, class: 'form-control'
- %div
- = link_to "Forgot your password?", reset_profile_password_path, method: :put
-
- .form-group
- = f.label :password, 'New password', class: 'control-label'
- .col-sm-10
+ = f.label :current_password, class: 'label-light'
+ = 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: 'control-label'
- .col-sm-10
+ .form-group
+ = f.label :password_confirmation, class: 'label-light'
= f.password_field :password_confirmation, required: true, class: 'form-control'
- .form-actions
- = f.submit 'Save password', class: "btn btn-create"
+ .prepend-top-default.append-bottom-default
+ = f.submit 'Save password', class: "btn btn-create append-right-10"
+ = link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link"
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 877589dc390..f80211669fb 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,57 +1,56 @@
- page_title 'Preferences'
- header_title page_title, profile_preferences_path
-- @blank_container = true
-.alert.alert-help
- These settings allow you to customize the appearance and behavior of the site.
- They are saved with your account and will persist to any device you use to
- access the site.
-
-= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f|
- .panel.panel-default.application-theme
- .panel-heading
+= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f|
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Application theme
- .panel-body
- - Gitlab::Themes.each do |theme|
- = label_tag do
- .preview{class: theme.css_class}
- = f.radio_button :theme_id, theme.id
- = theme.name
-
- .panel.panel-default.syntax-theme
- .panel-heading
+ %p
+ This setting allows you to customize the appearance of the site, ex. sidebar.
+ .col-lg-9.application-theme
+ - Gitlab::Themes.each do |theme|
+ = label_tag do
+ .preview{class: theme.css_class}
+ = f.radio_button :theme_id, theme.id
+ = theme.name
+ .col-sm-12
+ %hr
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Syntax highlighting theme
- .panel-body
- - Gitlab::ColorSchemes.each do |scheme|
- = label_tag do
- .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
- = f.radio_button :color_scheme_id, scheme.id
- = scheme.name
-
- .panel.panel-default
- .panel-heading
+ %p
+ This setting allow you to customize the appearance of the syntax.
+ .col-lg-9.syntax-theme
+ - Gitlab::ColorSchemes.each do |scheme|
+ = label_tag do
+ .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
+ = f.radio_button :color_scheme_id, scheme.id
+ = scheme.name
+ .col-sm-12
+ %hr
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Behavior
- .panel-body
- .form-group
- = f.label :layout, class: 'control-label' do
- Layout width
- .col-sm-10
- = f.select :layout, layout_choices, {}, class: 'form-control'
- .help-block
- Choose between fixed (max. 1200px) and fluid (100%) application layout.
- .form-group
- = f.label :dashboard, class: 'control-label' do
- Default Dashboard
- = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
- .col-sm-10
- = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
- .form-group
- = f.label :project_view, class: 'control-label' do
- Project view
- = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
- .col-sm-10
- = f.select :project_view, project_view_choices, {}, class: 'form-control'
- .help-block
- Choose what content you want to see on a project's home page.
- .panel-footer
+ %p
+ This setting allows you to customize the behavior of the system layout and default views.
+ .col-lg-9
+ .form-group
+ = f.label :layout, class: 'label-light' do
+ Layout width
+ = f.select :layout, layout_choices, {}, class: 'form-control'
+ .help-block
+ Choose between fixed (max. 1200px) and fluid (100%) application layout.
+ .form-group
+ = f.label :dashboard, class: 'label-light' do
+ Default Dashboard
+ = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
+ = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
+ .form-group
+ = f.label :project_view, class: 'label-light' do
+ Project view
+ = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
+ = f.select :project_view, project_view_choices, {}, class: 'form-control'
+ .help-block
+ Choose what content you want to see on a project's home page.
+ .form-group
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 9459d8a6295..cd582ba7060 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,101 +1,96 @@
-.gray-content-block.top-block
- This information will appear on your profile.
- - if current_user.ldap_user?
- Some options are unavailable for LDAP accounts
-
-.prepend-top-default
-= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit_user form-horizontal" }, authenticity_token: true do |f|
+= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
-if @user.errors.any?
%div.alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
.row
- .col-md-7
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Public Avatar
+ %p
+ - if @user.avatar?
+ You can change your avatar here
+ - if Gitlab.config.gravatar.enabled
+ or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
+ - else
+ You can upload an avatar here
+ - if Gitlab.config.gravatar.enabled
+ or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
+ .col-lg-9
+ .clearfix.avatar-image.append-bottom-default
+ = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
+ %h5.prepend-top-0
+ Upload new avatar
+ .prepend-top-5.append-bottom-10
+ %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"
+ .help-block
+ The maximum file size allowed is 200KB.
+ - if @user.avatar?
+ %hr
+ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-gray"
+ %hr
+ .row
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Main settings
+ %p
+ This information will appear on your profile.
+ - if current_user.ldap_user?
+ Some options are unavailable for LDAP accounts
+ .col-lg-9
.form-group
- = f.label :name, class: "control-label"
- .col-sm-10
- = f.text_field :name, class: "form-control", required: true
- %span.help-block Enter your name, so people you know can recognize you.
+ = f.label :name, class: "label-light"
+ = f.text_field :name, class: "form-control", required: true
+ %span.help-block Enter your name, so people you know can recognize you.
.form-group
- = f.label :email, class: "control-label"
- .col-sm-10
- - if @user.ldap_user?
- = f.text_field :email, class: "form-control", required: true, readonly: true
- %span.help-block.light
- Email is read-only for LDAP user
+ = f.label :email, class: "label-light"
+ - if @user.ldap_user? && @user.ldap_email?
+ = f.text_field :email, class: "form-control", required: true, readonly: true
+ %span.help-block.light
+ Your email address was automatically set based on the LDAP server.
+ - else
+ - if @user.temp_oauth_email?
+ = f.text_field :email, class: "form-control", required: true, value: nil
- else
- - if @user.temp_oauth_email?
- = f.text_field :email, class: "form-control", required: true, value: nil
- - else
- = f.text_field :email, class: "form-control", required: true
- - if @user.unconfirmed_email.present?
- %span.help-block
- Please click the link in the confirmation email before continuing. It was sent to
- = succeed "." do
- %strong #{@user.unconfirmed_email}
- %p
- = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
+ = f.text_field :email, class: "form-control", required: true
+ - if @user.unconfirmed_email.present?
+ %span.help-block
+ Please click the link in the confirmation email before continuing. It was sent to
+ = succeed "." do
+ %strong #{@user.unconfirmed_email}
+ %p
+ = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
- - else
- %span.help-block We also use email for avatar detection if no avatar is uploaded.
+ - else
+ %span.help-block We also use email for avatar detection if no avatar is uploaded.
.form-group
- = f.label :public_email, class: "control-label"
- .col-sm-10
- = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
- %span.help-block This email will be displayed on your public profile.
+ = f.label :public_email, class: "label-light"
+ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
+ %span.help-block This email will be displayed on your public profile.
.form-group
- = f.label :skype, class: "control-label"
- .col-sm-10= f.text_field :skype, class: "form-control"
+ = f.label :skype, class: "label-light"
+ = f.text_field :skype, class: "form-control"
.form-group
- = f.label :linkedin, class: "control-label"
- .col-sm-10= f.text_field :linkedin, class: "form-control"
+ = f.label :linkedin, class: "label-light"
+ = f.text_field :linkedin, class: "form-control"
.form-group
- = f.label :twitter, class: "control-label"
- .col-sm-10= f.text_field :twitter, class: "form-control"
+ = f.label :twitter, class: "label-light"
+ = f.text_field :twitter, class: "form-control"
.form-group
- = f.label :website_url, 'Website', class: "control-label"
- .col-sm-10= f.text_field :website_url, class: "form-control"
+ = f.label :website_url, 'Website', class: "label-light"
+ = f.text_field :website_url, class: "form-control"
.form-group
- = f.label :location, 'Location', class: "control-label"
- .col-sm-10= f.text_field :location, class: "form-control"
+ = f.label :location, 'Location', class: "label-light"
+ = f.text_field :location, class: "form-control"
.form-group
- = f.label :bio, class: "control-label"
- .col-sm-10
- = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
- %span.help-block Tell us about yourself in fewer than 250 characters.
-
- .col-md-5
- .light-well
- = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
-
- .clearfix
- .profile-avatar-form-option
- %p.light
- - if @user.avatar?
- You can change your avatar here
- - if Gitlab.config.gravatar.enabled
- %br
- or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
- - else
- You can upload an avatar here
- - if Gitlab.config.gravatar.enabled
- %br
- or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
- %hr
- %a.choose-btn.btn.btn-sm.js-choose-user-avatar-button
- %i.fa.fa-paperclip
- %span Choose File ...
- &nbsp;
- %span.file_name.js-avatar-filename File name...
- = f.file_field :avatar, class: "js-user-avatar-input hidden"
- .light The maximum file size allowed is 200KB.
- - if @user.avatar?
- %hr
- = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
-
-
- .form-actions
- = f.submit 'Save changes', class: "btn btn-success"
- = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
+ = f.label :bio, class: "label-light"
+ = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
+ %span.help-block Tell us about yourself in fewer than 250 characters.
+ .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"
diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml
index 1a5b6efce35..5d342ef58e5 100644
--- a/app/views/profiles/two_factor_auths/new.html.haml
+++ b/app/views/profiles/two_factor_auths/new.html.haml
@@ -1,41 +1,41 @@
- page_title 'Two-factor Authentication', 'Account'
-%h2.page-title Two-Factor Authentication (2FA)
-%p
- Download the Google Authenticator application from App Store for iOS or Google
- Play for Android and scan this code.
-
- More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
-
-%hr
-
-= form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal two-factor-new' do |f|
- - if @error
- .alert.alert-danger
- = @error
- .form-group
- .col-lg-2.col-lg-offset-2
- = raw @qr_code
- .col-lg-7.col-lg-offset-1.manual-instructions
- %h3 Can't scan the code?
-
- %p
- To add the entry manually, provide the following details to the
- application on your phone.
-
- %dl
- %dt Account
- %dd= current_user.email
- %dl
- %dt Key
- %dd= current_user.otp_secret.scan(/.{4}/).join(' ')
- %dl
- %dt Time based
- %dd Yes
- .form-group
- = label_tag :pin_code, nil, class: "control-label"
- .col-lg-10
- = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
- .form-actions
- = submit_tag 'Submit', class: 'btn btn-success'
- = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Two-factor Authentication (2FA)
+ %p
+ Increase your account's security by enabling two-factor authentication (2FA).
+ .col-lg-9
+ %p
+ Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
+ %p
+ Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
+ More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
+ .row.append-bottom-10
+ .col-md-3
+ = raw @qr_code
+ .col-md-9
+ .account-well
+ %p.prepend-top-0.append-bottom-0
+ Can't scan the code?
+ %p.prepend-top-0.append-bottom-0
+ To add the entry manually, provide the following details to the application on your phone.
+ %p.prepend-top-0.append-bottom-0
+ Account:
+ = current_user.email
+ %p.prepend-top-0.append-bottom-0
+ Key:
+ = current_user.otp_secret.scan(/.{4}/).join(' ')
+ %p.two-factor-new-manual-content
+ Time based: Yes
+ = form_tag profile_two_factor_auth_path, method: :post do |f|
+ - if @error
+ .alert.alert-danger
+ = @error
+ .form-group
+ = label_tag :pin_code, nil, class: "label-light"
+ = text_field_tag :pin_code, nil, class: "form-control", required: true
+ .prepend-top-default
+ = submit_tag 'Enable two-factor authentication', class: 'btn btn-success'
+ = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 101880bd105..961b61d2e76 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,6 +1,6 @@
-.gray-content-block.activity-filter-block
+.nav-block.activity-filter-block
- if current_user
- .pull-right
+ .controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
%i.fa.fa-rss
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
new file mode 100644
index 00000000000..95ab9ecf3e8
--- /dev/null
+++ b/app/views/projects/_builds_settings.html.haml
@@ -0,0 +1,60 @@
+%fieldset.builds-feature
+ %legend
+ Builds:
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %p Get recent application code using the following command:
+ .radio
+ = f.label :build_allow_git_fetch_false do
+ = f.radio_button :build_allow_git_fetch, 'false'
+ %strong git clone
+ %br
+ %span.descr Slower but makes sure you have a clean dir before every build
+ .radio
+ = f.label :build_allow_git_fetch_true do
+ = f.radio_button :build_allow_git_fetch, 'true'
+ %strong git fetch
+ %br
+ %span.descr Faster
+
+ .form-group
+ = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
+ .col-sm-10
+ = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ %p.help-block per build in minutes
+ .form-group
+ = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
+ .col-sm-10
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
+ %span.input-group-addon /
+ %p.help-block
+ We will use this regular expression to find test coverage output in build trace.
+ Leave blank if you want to disable this feature
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%\s*$
+ %li
+ phpunit --coverage-text --colors=never (PHP) -
+ %code ^\s*Lines:\s*\d+.\d+\%
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :public_builds do
+ = f.check_box :public_builds
+ %strong Public builds
+ .help-block Allow everyone to access builds for Public and Internal projects
+
+ .form-group
+ = f.label :runners_token, "Runners token", class: 'control-label'
+ .col-sm-10
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used to checkout project.
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index fa978325ddd..96c2fa87f45 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,5 +1,5 @@
#tree-holder.tree-holder.clearfix
- .gray-content-block.second-block
+ .nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml
new file mode 100644
index 00000000000..08e2fc48be7
--- /dev/null
+++ b/app/views/projects/_find_file_link.html.haml
@@ -0,0 +1,3 @@
+= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
+ = icon('search')
+ %span Find File
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index e92115b9b98..b45df44f270 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -3,7 +3,12 @@
.project-identicon-holder
= project_icon(@project, alt: '', class: 'project-avatar avatar s90')
.project-home-desc
- %h1= @project.name
+ %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)}"}
+ = visibility_level_icon(@project.visibility_level, fw: false)
+
- if @project.description.present?
= markdown(@project.description, pipeline: :description)
@@ -12,32 +17,44 @@
Forked from
= link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
- .cover-controls.left
- .visibility-level-label.has_tooltip{title: project_visibility_level_description(@project.visibility_level), data: { container: 'body' } }
- = visibility_level_icon(@project.visibility_level, fw: false)
- = visibility_level_label(@project.visibility_level)
.cover-controls
- - if can?(current_user, :admin_project, @project)
- = link_to edit_project_path(@project), class: 'btn btn-gray' do
- = icon('pencil')
- if current_user
- &nbsp;
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), class: 'btn btn-gray' do
= icon('rss')
+ - access = user_max_access_in_project(current_user.id, @project)
+ - can_edit = can?(current_user, :admin_project, @project)
+ - if access || can_edit
+ %span.dropdown.project-settings-dropdown
+ %a.dropdown-new.btn.btn-gray#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('angle-down')
+ %ul.dropdown-menu.dropdown-menu-right
+ - if can_edit
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
+ - if access
+ %li
+ = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
+ data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
+ Leave Project
.project-repo-buttons
.split-one.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- = render "shared/clone_panel"
+ .clone-row
+ .project-clone-holder
+ = render "shared/clone_panel"
- .split-repo-buttons
- = render "projects/buttons/download"
- = render 'projects/buttons/dropdown'
+ .split-repo-buttons
+ .btn-group.pull-left
+ = render "projects/buttons/download"
+ = render 'projects/buttons/dropdown'
- = render 'projects/buttons/notifications'
+ = render 'projects/buttons/notifications'
-:coffeescript
- new Star() \ No newline at end of file
+:javascript
+ new Star();
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 54c818baaf4..1fb37ef6621 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,6 +1,6 @@
.md-area
.md-header.clearfix
- %ul.center-top-menu
+ %ul.nav-links
%li.active
%a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 7e6301abde8..e701253d7de 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,13 +1,12 @@
.zennable
- %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox")
.zen-backdrop
- - classes << ' js-gfm-input markdown-area'
+ - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
- = f.text_area attr, class: classes, placeholder: ''
+ = f.text_area attr, class: classes
- else
- = text_area_tag attr, nil, class: classes, placeholder: ''
- %a.zen-enter-link(tabindex="-1" href="#")
+ = text_area_tag attr, nil, class: classes
+ %a.js-zen-enter(tabindex="-1" href="#")
= icon('expand')
Edit in fullscreen
- %a.zen-leave-link(href="#")
+ %a.js-zen-leave(tabindex="-1" href="#")
= icon('compress')
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
new file mode 100644
index 00000000000..def493c56f5
--- /dev/null
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -0,0 +1,8 @@
+- path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path)
+
+%tr.tree-item{ 'data-link' => path_to_directory}
+ %td.tree-item-file-name
+ = tree_icon('folder', '755', directory.name)
+ %span.str-truncated
+ = link_to directory.name, path_to_directory
+ %td
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
new file mode 100644
index 00000000000..36fb4c998c9
--- /dev/null
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -0,0 +1,9 @@
+- path_to_file = file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path)
+
+%tr.tree-item{ 'data-link' => path_to_file }
+ %td.tree-item-file-name
+ = tree_icon('file', '664', file.name)
+ %span.str-truncated
+ = link_to file.name, path_to_file
+ %td
+ = number_to_human_size(file.metadata[:size], precision: 2)
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
new file mode 100644
index 00000000000..84034c8bf16
--- /dev/null
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -0,0 +1,22 @@
+- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
+= render 'projects/builds/header_title'
+
+.top-block.gray-content-block.clearfix
+ .pull-right
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
+ class: 'btn btn-default download' do
+ = icon('download')
+ Download artifacts archive
+
+.tree-holder
+ %div.tree-content-holder
+ %table.table.tree-table
+ %thead
+ %tr
+ %th Name
+ %th Size
+ = render partial: 'tree_directory', collection: @entry.directories(parent: true), as: :directory
+ = render partial: 'tree_file', collection: @entry.files, as: :file
+
+- if @entry.empty?
+ .center Empty
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 8d9ec068a43..5f9a92ff93f 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -3,7 +3,7 @@
%h3.page-title Blame view
-#tree-holder.tree-holder
+#blob-content-holder.tree-holder
.file-holder
.file-title
= blob_icon @blob.mode, @blob.name
@@ -12,14 +12,14 @@
%small= number_to_human_size @blob.size
.file-actions
= render "projects/blob/actions"
- .file-content.blame.highlight
+ .file-content.blame.code.js-syntax-highlight
%table
- current_line = 1
- - @blame.each do |blame_group|
+ - @blame_groups.each do |blame_group|
%tr
%td.blame-commit
.commit
- - commit = Commit.new(blame_group[:commit], @project)
+ - commit = blame_group[:commit]
.commit-row-title
%strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
@@ -30,16 +30,16 @@
= commit_author_link(commit, avatar: false)
authored
#{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
- %td.lines.blame-numbers
- %pre
- - line_count = blame_group[:lines].count
- - (current_line...(current_line + line_count)).each do |i|
+ %td.line-numbers
+ - line_count = blame_group[:lines].count
+ - (current_line...(current_line + line_count)).each do |i|
+ %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ = icon("link")
= i
- \
- - current_line += line_count
+ \
+ - current_line += line_count
%td.lines
- %pre{class: 'code highlight white'}
+ %pre.code.highlight
%code
- blame_group[:lines].each do |line|
- :erb
- <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %>
+ #{line}
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 2a3315da3db..3ffc3fcb7ac 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -1,4 +1,4 @@
-.gray-content-block.top-block
+.nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
@@ -32,11 +32,4 @@
= number_to_human_size(blob_size(blob))
.file-actions.hidden-xs
= render "actions"
- - if blob.lfs_pointer?
- = render "download", blob: blob
- - elsif blob.text?
- = render "text", blob: blob
- - elsif blob.image?
- = render "image", blob: blob
- - else
- = render "download", blob: blob
+ = render blob, blob: blob
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 10b02813733..f8b6fa253c4 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -10,7 +10,7 @@
%span.editor-file-name
\/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
- required: true, class: 'form-control new-file-name js-quick-submit'
+ required: true, class: 'form-control new-file-name'
.pull-right
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index c090f690d1d..18caddabd39 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -1,2 +1,9 @@
.file-content.image_file
- %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+ - if blob.svg?
+ - # We need to scrub SVG but we cannot do so in the RawController: it would
+ - # be wrong/strange if RawController modified the data.
+ - blob.load_all_data!(@repository)
+ - blob = sanitize_svg(blob)
+ %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+ - else
+ %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))}
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 084608bbba3..84694203d7d 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title Create New Directory
.modal-body
- = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do
+ = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do
.form-group
= label_tag :dir_name, 'Directory name', class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index 1cf19a7d3db..2e1f32fd15e 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -6,7 +6,7 @@
%h3.page-title Delete #{@blob.name}
.modal-body
- = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do
+ = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-quick-submit js-requires-input' do
= render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
.form-group
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index 4429c395aee..d09cd73558c 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -1,9 +1,10 @@
+- blob.load_all_data!(@repository)
- if markup?(blob.name)
.file-content.wiki
= render_markup(blob.name, blob.data)
- else
- .file-content.code
- - unless blob.empty?
- = render 'shared/file_highlight', blob: blob
- - else
+ - unless blob.empty?
+ = render 'shared/file_highlight', blob: blob
+ - else
+ .file-content.code
.nothing-here-block Empty file
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 676924dc6ca..b1f50eb5f34 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title #{title}
.modal-body
- = form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do
+ = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index f3b01ff3288..abcfca4cd11 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -10,8 +10,9 @@
%tr.line_holder
%td.old_line.diff-line-num{data: {linenumber: line_old}}
= link_to raw(line_old), "#"
- %td.new_line= link_to raw(line_new) , "#"
- %td.line_content.noteable_line= ' ' * @form.indent + line
+ %td.new_line.diff-line-num
+ = link_to raw(line_new) , "#"
+ %td.line_content.noteable_line==#{' ' * @form.indent}#{line}
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc
%tr.line_holder{ id: @form.to }
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 09fa148b129..effcce5a1c4 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,7 +2,7 @@
= render "header_title"
.file-editor
- %ul.center-top-menu.no-bottom.js-edit-mode
+ %ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
= icon('edit')
@@ -13,7 +13,7 @@
= icon('eye')
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 167fa615182..1dd2b5c0af7 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -5,7 +5,7 @@
New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index e7c3460ad78..541dc96c45f 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -8,18 +8,18 @@
.file-content.wiki
= raw render_markup(@blob.name, @content)
- else
- .file-content.code
+ .file-content.code.js-syntax-highlight
- unless @diff_lines.empty?
%table.text-file
- @diff_lines.each do |line|
%tr.line_holder{ class: "#{line.type}" }
- if line.type == "match"
- %td.old_line= "..."
- %td.new_line= "..."
- %td.line_content.matched= line.text
+ %td.old_line.diff-line-num= "..."
+ %td.new_line.diff-line-num= "..."
+ %td.line_content.match= line.text
- else
- %td.old_line
- %td.new_line
- %td.line_content{class: "#{line.type}"}= raw diff_line_content(line.text)
+ %td.old_line.diff-line-num
+ %td.new_line.diff-line-num
+ %td.line_content{class: "#{line.type}"}= diff_line_content(line.text)
- else
.nothing-here-block No changes.
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 5081bae6801..76a823d3828 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,8 +1,12 @@
- commit = @repository.commit(branch.target)
+- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
+- diverging_commit_counts = @repository.diverging_commit_counts(branch)
+- number_commits_behind = diverging_commit_counts[:behind]
+- number_commits_ahead = diverging_commit_counts[:ahead]
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
- %strong.str-truncated= branch.name
+ %span.item-title.str-truncated= branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
@@ -29,6 +33,17 @@
= 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
+ .divergence-graph{ title: "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" }
+ .graph-side
+ .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
+ %span.count.count-behind= number_commits_behind
+ .graph-separator
+ .graph-side
+ .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
+ %span.count.count-ahead= number_commits_ahead
+
+
- if commit
= render 'projects/branches/commit', commit: commit, project: @project
- else
diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml
index 882a4d0c5e2..a21ddaf4930 100644
--- a/app/views/projects/branches/destroy.js.haml
+++ b/app/views/projects/branches/destroy.js.haml
@@ -1 +1 @@
-$('.js-totalbranch-count').html("#{@repository.branches.size}")
+$('.js-totalbranch-count').html("#{@repository.branch_count}")
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 204def60794..7afea5a5049 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -10,7 +10,7 @@
&nbsp;
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
+ %span.light
- if @sort.present?
= @sort.humanize
- else
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 1a26908ab11..811d304ea75 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -1,16 +1,16 @@
- page_title "Builds"
= render "header_title"
-.project-issuable-filter
- .controls
- - if can?(current_user, :manage_builds, @project)
- .pull-left.hidden-xs
- - if @all_builds.running_or_pending.any?
- = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
-
- %ul.center-top-menu
+.top-area
+ %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do
+ All
+ %span.badge.js-totalbuilds-count
+ = number_with_delimiter(@all_builds.count(:id))
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to project_builds_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@all_builds.running_or_pending.count(:id))
@@ -21,11 +21,15 @@
%span.badge.js-running-count
= number_with_delimiter(@all_builds.finished.count(:id))
- %li{class: ('active' if @scope == 'all')}
- = link_to project_builds_path(@project, scope: :all) do
- All
- %span.badge.js-totalbuilds-count
- = number_with_delimiter(@all_builds.count(:id))
+ .nav-controls
+ - if can?(current_user, :update_build, @project)
+ - if @all_builds.running_or_pending.any?
+ = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project),
+ data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+
+ = link_to ci_lint_path, class: 'btn btn-default' do
+ = icon('wrench')
+ %span CI Lint
.gray-content-block
#{(@scope || 'running').capitalize} builds from this project
@@ -40,16 +44,17 @@
%thead
%tr
%th Status
- %th Runner
+ %th Build ID
%th Commit
%th Ref
%th Stage
%th Name
%th Duration
%th Finished at
+ - if @project.build_coverage_enabled?
+ %th Coverage
%th
- - @builds.each do |build|
- = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true
+ = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
= paginate @builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 5b7ecce86ab..b02aee3db21 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -13,9 +13,10 @@
= link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
#up-build-trace
- - if @commit.matrix_for_ref?(@build.ref)
- %ul.center-top-menu.no-top.no-bottom
- - @commit.latest_builds_for_ref(@build.ref).each do |build|
+ - builds = @build.commit.matrix_builds(@build)
+ - if builds.size > 1
+ %ul.nav-links.no-top.no-bottom
+ - builds.each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= ci_icon_for_status(build.status)
@@ -44,7 +45,7 @@
.pull-right
#{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
- - if @build.show_warning?
+ - if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
%p
@@ -70,16 +71,22 @@
.autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
.clearfix
- .scroll-controls
+ #js-build-scroll.scroll-controls
= link_to '#up-build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
- %pre.trace#build-trace
- %code.bash
- = preserve do
- = raw @build.trace_html
+ - if @build.erased?
+ .erased.alert.alert-warning
+ - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
+ Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
+ - else
+ %pre.trace#build-trace
+ %code.bash
+ = preserve do
+ = raw @build.trace_html
+
%div#down-build-trace
.col-md-3
@@ -89,37 +96,60 @@
Test coverage
%h1 #{@build.coverage}%
- - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url
- .build-widget.center
- = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary'
+ - if can?(current_user, :read_build, @project) && @build.artifacts?
+ .build-widget.artifacts
+ %h4.title Build artifacts
+ .center
+ .btn-group{ role: :group }
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
+ = icon('download')
+ Download
+
+ - if @build.artifacts_metadata?
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
+ = icon('folder-open')
+ Browse
.build-widget
%h4.title
Build ##{@build.id}
- - if current_user && can?(current_user, :manage_builds, @project)
- .pull-right
- - if @build.cancel_url
- = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post
- - elsif @build.retry_url
- = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post
-
- - if @build.duration
+ - if can?(current_user, :update_build, @project)
+ .center
+ .btn-group{ role: :group }
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post
+ - elsif @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
+
+ - if @build.erasable?
+ = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
+ class: 'btn btn-sm btn-warning', method: :post,
+ data: { confirm: 'Are you sure you want to erase this build?' } do
+ = icon('eraser')
+ Erase
+
+ .clearfix
+ - if @build.duration
+ %p
+ %span.attr-name Duration:
+ #{duration_in_words(@build.finished_at, @build.started_at)}
%p
- %span.attr-name Duration:
- #{duration_in_words(@build.finished_at, @build.started_at)}
- %p
- %span.attr-name Created:
- #{time_ago_with_tooltip(@build.created_at)}
- - if @build.finished_at
+ %span.attr-name Created:
+ #{time_ago_with_tooltip(@build.created_at)}
+ - if @build.finished_at
+ %p
+ %span.attr-name Finished:
+ #{time_ago_with_tooltip(@build.finished_at)}
+ - if @build.erased_at
+ %p
+ %span.attr-name Erased:
+ #{time_ago_with_tooltip(@build.erased_at)}
%p
- %span.attr-name Finished:
- #{time_ago_with_tooltip(@build.finished_at)}
- %p
- %span.attr-name Runner:
- - if @build.runner && current_user && current_user.admin
- = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
- - elsif @build.runner
- \##{@build.runner.id}
+ %span.attr-name Runner:
+ - if @build.runner && current_user && current_user.admin
+ = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
+ - elsif @build.runner
+ \##{@build.runner.id}
- if @build.trigger_request
.build-widget
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 14ee2263b7d..6a60cfeff76 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', 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 1f639fecc30..e7c85edff96 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,6 +1,6 @@
- if current_user
- %span.dropdown
- %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
+ .btn-group
+ %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)
@@ -8,9 +8,10 @@
= link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- - if can?(current_user, :create_merge_request, @project)
+ - 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(@project.namespace, @project) do
+ = 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)
@@ -45,7 +46,7 @@
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('file fw')
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..d22d1da8402
--- /dev/null
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -0,0 +1,76 @@
+%tr.build
+ %td.status
+ - if can?(current_user, :read_build, build)
+ = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
+ - else
+ = ci_status_with_icon(build.status)
+
+ %td.build-link
+ - if can?(current_user, :read_build, build)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
+ %strong ##{build.id}
+ - else
+ %strong ##{build.id}
+
+ - if build.stuck?
+ %i.fa.fa-warning.text-warning
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
+
+ %td
+ - if build.ref
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if build.try(:runner)
+ = runner_link(build.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = build.stage
+
+ %td
+ = build.name
+
+ .pull-right
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_with_tooltip(build.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if build.try(:coverage)
+ #{build.coverage}%
+
+ %td
+ .pull-right
+ - if can?(current_user, :read_build, build) && build.artifacts?
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
+ %i.fa.fa-download
+ - if can?(current_user, :update_build, build)
+ - if build.active?
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
+ %i.fa.fa-repeat
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index 329aaa0bb8b..003b7c18d0e 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -1,6 +1,6 @@
.gray-content-block.middle-block
.pull-right
- - if can?(current_user, :manage_builds, @ci_commit.project)
+ - if can?(current_user, :update_build, @ci_commit.project)
- if @ci_commit.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post
@@ -43,8 +43,8 @@
%th Coverage
%th
- @ci_commit.refs.each do |ref|
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true }
+ - builds = @ci_commit.statuses.for_ref(ref).latest.ordered
+ = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true
- if @ci_commit.retried.any?
.gray-content-block.second-block
@@ -64,5 +64,4 @@
- if @ci_commit.project.build_coverage_enabled?
%th Coverage
%th
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true }
+ = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index f74f8b427ec..ea33aa472a6 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu.no-top.no-bottom.commit-ci-menu
+%ul.nav-links.no-top.no-bottom.commit-ci-menu
= nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index ddb77fd796b..71995fcc487 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -16,6 +16,8 @@
= link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do
= icon('files-o')
Browse Files
+ - unless @commit.has_been_reverted?(current_user)
+ = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id))
%div
%p
@@ -50,7 +52,7 @@
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
-.commit-box.gray-content-block.middle-block
+.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
diff --git a/app/views/projects/commit/_revert.html.haml b/app/views/projects/commit/_revert.html.haml
new file mode 100644
index 00000000000..52ca3ed5b14
--- /dev/null
+++ b/app/views/projects/commit/_revert.html.haml
@@ -0,0 +1,31 @@
+#modal-revert-commit.modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %h3.page-title== Revert this #{revert_commit_type(commit)}
+ .modal-body
+ = form_tag revert_namespace_project_commit_path(@project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do
+ .form-group.branch
+ = label_tag 'target_branch', 'Revert in branch', class: 'control-label'
+ .col-sm-10
+ = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch"
+ - if can?(current_user, :push_code, @project)
+ .js-create-merge-request-container
+ .checkbox
+ - nonce = SecureRandom.hex
+ = label_tag "create_merge_request-#{nonce}" do
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
+ Start a <strong>new merge request</strong> with these changes
+ - else
+ = hidden_field_tag 'create_merge_request', 1
+ .form-actions
+ = submit_tag "Revert", class: 'btn btn-create'
+ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+ - unless can?(current_user, :push_code, @project)
+ .inline.prepend-left-10
+ = commit_in_fork_help
+
+:javascript
+ new NewCommitForm($('.js-create-dir-form'))
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
index 99d62503a94..7118a4846c6 100644
--- a/app/views/projects/commit/builds.html.haml
+++ b/app/views/projects/commit/builds.html.haml
@@ -1,6 +1,7 @@
- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
= render "projects/commits/header_title"
-= render "commit_box"
+.prepend-top-default
+ = render "commit_box"
= render "ci_menu"
= render "builds"
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 069b8b1f169..21e186120c3 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,9 +1,16 @@
-- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_description @commit.description
+
= render "projects/commits/header_title"
-= render "commit_box"
+
+.prepend-top-default
+ = render "commit_box"
- if @ci_commit
= render "ci_menu"
- else
%div.block-connector
-= render "projects/diffs/diffs", diffs: @diffs, project: @project
+= render "projects/diffs/diffs", diffs: @diffs, project: @project,
+ diff_refs: @diff_refs
= render "projects/notes/notes_with_form"
+- if can_collaborate_with_project?
+ = render "projects/commit/revert", commit: @commit, title: @commit.title
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
deleted file mode 100644
index 74a05df24d3..00000000000
--- a/app/views/projects/commit_statuses/_commit_status.html.haml
+++ /dev/null
@@ -1,79 +0,0 @@
-%tr.commit_status
- %td.status
- - if commit_status.target_url
- = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do
- = ci_icon_for_status(commit_status.status)
- = commit_status.status
- - else
- = ci_status_with_icon(commit_status.status)
-
- %td.commit_status-link
- - if commit_status.target_url
- = link_to commit_status.target_url do
- %strong ##{commit_status.id}
- - else
- %strong ##{commit_status.id}
-
- - if commit_status.show_warning?
- %i.fa.fa-warning.text-warning
-
- - if defined?(commit_sha) && commit_sha
- %td
- = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace"
-
- %td
- - if commit_status.ref
- = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref)
- - else
- .light none
-
- - if defined?(runner) && runner
- %td
- - if commit_status.try(:runner)
- = runner_link(commit_status.runner)
- - else
- .light none
-
- - if defined?(stage) && stage
- %td
- = commit_status.stage
-
- %td
- = commit_status.name
-
- .pull-right
- - if commit_status.tags.any?
- - commit_status.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if commit_status.try(:trigger_request)
- %span.label.label-info triggered
- - if commit_status.try(:allow_failure)
- %span.label.label-danger allowed to fail
-
- %td.duration
- - if commit_status.duration
- #{duration_in_words(commit_status.finished_at, commit_status.started_at)}
-
- %td.timestamp
- - if commit_status.finished_at
- %span #{time_ago_with_tooltip(commit_status.finished_at)}
-
- - if defined?(coverage) && coverage
- %td.coverage
- - if commit_status.try(:coverage)
- #{commit_status.coverage}%
-
- %td
- .pull-right
- - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url
- = link_to commit_status.download_url, title: 'Download artifacts' do
- %i.fa.fa-download
- - if current_user && can?(current_user, :manage_builds, commit_status.project)
- - if commit_status.active?
- - if commit_status.cancel_url
- = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && commit_status.retry_url
- = link_to commit_status.retry_url, method: :post, title: 'Retry' do
- %i.fa.fa-repeat
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 012825f0fdb..7f2903589a9 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -11,7 +11,7 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
.commit-row-title
- %strong.str-truncated
+ %span.item-title.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- if commit.description?
%a.text-expander.js-toggle-button ...
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index ce60fbdf032..bac9e244d36 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -1,11 +1,14 @@
+- commits, hidden = limited_commits(@commits)
+- commits = Commit.decorate(commits, @project)
+
%div.panel.panel-default
.panel-heading
Commits (#{@commits.count})
- - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ - if hidden > 0
%ul.well-list
- - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit|
+ - commits.each do |commit|
= render "projects/commits/inline_commit", commit: commit, project: @project
%li.warning-row.unstyled
- other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues.
+ #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
- %ul.well-list= render Commit.decorate(@commits, @project), project: @project
+ %ul.well-list= render commits, project: @project
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 0cd9ce1f371..a7e3c2478c2 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -1,15 +1,21 @@
- unless defined?(project)
- project = @project
-- @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
+- commits, hidden = limited_commits(@commits)
+
+- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
.row.commits-row
.col-md-2.hidden-xs.hidden-sm
%h5.commits-row-date
%i.fa.fa-calendar
- %span= day.stamp("28 Aug, 2010")
+ %span= day.strftime('%d %b, %Y')
.light
= pluralize(commits.count, 'commit')
.col-md-10.col-sm-12
%ul.bordered-list
= render commits, project: project
%hr.lists-separator
+
+- if hidden > 0
+ .alert.alert-warning
+ #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index fcccb002d7e..7a5b0d993db 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
@@ -15,9 +15,9 @@
= nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do
Branches
- %span.badge.js-totalbranch-count= @repository.branches.size
+ %span.badge.js-totalbranch-count= @repository.branch_count
= nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
- %span.badge.js-totaltags-count= @repository.tags.length
+ %span.badge.js-totaltags-count= @repository.tag_count
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 7ffa7317196..e310fafd82c 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -4,14 +4,14 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html"
xml.id namespace_project_commits_url(@project.namespace, @project, @ref)
- xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any?
+ xml.updated @commits.first.committed_date.xmlschema if @commits.any?
@commits.each do |commit|
xml.entry do
xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id)
xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id)
xml.title truncate(commit.title, length: 80)
- xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.updated commit.committed_date.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email))
xml.author do |author|
xml.name commit.author_name
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 2dd99cc8215..c52cf25d40a 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -6,30 +6,37 @@
= render "head"
-.gray-content-block
+.gray-content-block.second-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
- .commits-feed-holder.hidden-xs.hidden-sm
- - if create_mr_button?(@repository.root_ref, @ref)
- = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
- = icon('plus')
- Create Merge Request
+ .block-controls.hidden-xs.hidden-sm
+ - if @merge_request.present?
+ .control
+ = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
+ - elsif create_mr_button?(@repository.root_ref, @ref)
+ .control
+ = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
+ = icon('plus')
+ Create Merge Request
+
+ .control
+ = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do
+ = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false }
- if current_user && current_user.private_token
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'prepend-left-10 btn' do
- = icon("rss")
+ .control
+ = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do
+ = icon("rss")
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
%div{id: dom_id(@project)}
- #commits-list= render "commits", project: @project
+ #commits-list.content_list= render "commits", project: @project
.clear
= spinner
-- if @commits.count == @limit
- :javascript
- CommitsList.init("#{@ref}", #{@limit});
-
+:javascript
+ CommitsList.init("#{@ref}", #{@limit});
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index efc25eda26b..4ab81f3635c 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -13,12 +13,13 @@
= text_field_tag :to, params[:to], class: "form-control", required: true
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
- - if create_mr_button?
+ - if @merge_request.present?
+ = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn'
+ - elsif create_mr_button?
= link_to create_mr_path, class: 'prepend-left-10 btn' do
= icon("plus")
Create Merge Request
-
:javascript
var availableTags = #{@project.repository.ref_names.to_json};
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 51088a7dea8..da731f28bb6 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -9,7 +9,7 @@
- if @commits.present?
.prepend-top-default
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs, project: @project
+ = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs
- else
.light-well.prepend-top-default
.center
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index f9d661d59d2..6086ad3661e 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,17 +1,17 @@
- if diff_view == 'parallel'
- fluid_layout true
-- diff_files = safe_diff_files(diffs)
+- diff_files = safe_diff_files(diffs, diff_refs)
-.gray-content-block.middle-block.oneline-block
+.content-block.oneline-block
.inline-parallel-buttons
.btn-group
= inline_diff_btn
= parallel_diff_btn
= render 'projects/diffs/stats', diff_files: diff_files
-- if diff_files.count < diffs.size
- = render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count
+- if diff_files.overflow?
+ = render 'projects/diffs/warning', diff_files: diff_files
.files
- diff_files.each_with_index do |diff_file, index|
@@ -21,10 +21,3 @@
= render 'projects/diffs/file', i: index, project: project,
diff_file: diff_file, diff_commit: diff_commit, blob: blob
-
-- if @diff_timeout
- .alert.alert-danger
- %h4
- Failed to collect changes
- %p
- Maybe diff is really big and operation failed with timeout. Try to get diff locally
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 517f6aef7c5..dc34032b1b8 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,52 +1,58 @@
-.diff-file{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)}
- .diff-header{id: "file-path-#{hexdigest(diff_file.file_path)}"}
+.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)}
+ .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
- if diff_file.diff.submodule?
%span
= icon('archive fw')
%strong
= submodule_link(blob, @commit.id, project.repository)
- else
- %span
- = blob_icon blob.mode, blob.name
- = link_to "#diff-#{i}" do
- %strong
- = diff_file.new_path
+ = blob_icon blob.mode, blob.name
- - if diff_file.deleted_file
- deleted
- - elsif diff_file.renamed_file
- renamed from
+ = link_to "#diff-#{i}" do
+ - if diff_file.renamed_file
+ - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ %strong.filename.old
+ = old_path
+ &rarr;
+ %strong.filename.new
+ = new_path
+ - else
%strong
- = diff_file.old_path
+ = diff_file.new_path
+ - if diff_file.deleted_file
+ deleted
- - if diff_file.mode_changed?
- %small
- = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
+ - if diff_file.mode_changed?
+ %small
+ = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
- .diff-controls
+ .file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do
- %i.fa.fa-comments
- &nbsp;
+ = link_to '#', class: 'js-toggle-diff-comments btn active has_tooltip', title: "Toggle comments for this file" do
+ = icon('comments')
+ \
- if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project,
@merge_request.source_branch, diff_file.new_path,
from_merge_request_id: @merge_request.id)
- &nbsp;
= view_file_btn(diff_commit.id, diff_file, project)
.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
+ - 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 058b71b21f5..752e92e2e6b 100644
--- a/app/views/projects/diffs/_image.html.haml
+++ b/app/views/projects/diffs/_image.html.haml
@@ -1,9 +1,11 @@
- 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))
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
%span.wrap
.frame{class: image_diff_class(diff)}
- %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
+ %img{src: diff.deleted_file ? old_file_raw_path : file_raw_path}
%p.image-info= "#{number_to_human_size file.size}"
- else
.image
@@ -11,7 +13,7 @@
%span.wrap
.frame.deleted
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))}
- %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"}
+ %img{src: old_file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
|
@@ -23,7 +25,7 @@
%span.wrap
.frame.added
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))}
- %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
+ %img{src: file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size file.size}"
|
@@ -36,10 +38,10 @@
%div.swipe.view.hide
.swipe-frame
.frame.deleted
- %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"}
+ %img{src: old_file_raw_path}
.swipe-wrap
.frame.added
- %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
+ %img{src: file_raw_path}
%span.swipe-bar
%span.top-handle
%span.bottom-handle
@@ -47,9 +49,9 @@
%div.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"}
+ %img{src: old_file_raw_path}
.frame.added
- %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
+ %img{src: file_raw_path}
.controls
.transparent
.drag-track
diff --git a/app/views/projects/diffs/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml
index d1f897b99f7..d6dddd97879 100644
--- a/app/views/projects/diffs/_match_line.html.haml
+++ b/app/views/projects/diffs/_match_line.html.haml
@@ -4,4 +4,4 @@
%td.new_line.diff-line-num{data: {linenumber: line_new},
class: [unfold_bottom_class(bottom), unfold_class(!new_file)]}
\...
-%td.line_content.matched= line
+%td.line_content.match= line
diff --git a/app/views/projects/diffs/_match_line_parallel.html.haml b/app/views/projects/diffs/_match_line_parallel.html.haml
index 815df16aa4a..0cd888876e0 100644
--- a/app/views/projects/diffs/_match_line_parallel.html.haml
+++ b/app/views/projects/diffs/_match_line_parallel.html.haml
@@ -1,4 +1,4 @@
-%td.old_line
- %td.line_content.parallel.matched= line
-%td.new_line
- %td.line_content.parallel.matched= line
+%td.old_line.diff-line-num
+%td.line_content.parallel.match= line
+%td.new_line.diff-line-num
+%td.line_content.parallel.match= line
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 37fd1b1ec8a..d7c49068745 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,42 +1,40 @@
/ Side-by-side diff view
-%div.text-file.diff-wrap-lines
+%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight
%table
- - parallel_diff(diff_file, index).each do |line|
- - type_left = line[0]
- - line_number_left = line[1]
- - line_content_left = line[2]
- - line_code_left = line[3]
- - type_right = line[4]
- - line_number_right = line[5]
- - line_content_right = line[6]
- - line_code_right = line[7]
-
+ - diff_file.parallel_diff_lines.each do |line|
+ - left = line[:left]
+ - right = line[:right]
%tr.line_holder.parallel
- - if type_left == 'match'
- = render "projects/diffs/match_line_parallel", { line: line_content_left,
- line_old: line_number_left, line_new: line_number_right }
- - elsif type_left == 'old' || type_left.nil?
- %td.old_line{id: line_code_left, class: "#{type_left}"}
- = link_to raw(line_number_left), "##{line_code_left}", id: line_code_left
+ - if left[:type] == 'match'
+ = render "projects/diffs/match_line_parallel", { line: left[:text],
+ line_old: left[:number], line_new: right[:number] }
+ - elsif left[:type] == 'nonewline'
+ %td.old_line.diff-line-num
+ %td.line_content.parallel.match= left[:text]
+ %td.new_line.diff-line-num
+ %td.line_content.parallel.match= left[:text]
+ - else
+ %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"}
+ = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code]
- if @comments_allowed && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(line_code_left, 'old')
- %td.line_content{class: "parallel noteable_line #{type_left} #{line_code_left}", "line_code" => line_code_left }= raw line_content_left
+ = link_to_new_diff_note(left[:line_code], 'old')
+ %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text])
- - if type_right == 'new'
+ - if right[:type] == 'new'
- new_line_class = 'new'
- - new_line_code = line_code_right
+ - new_line_code = right[:line_code]
- else
- new_line_class = nil
- - new_line_code = line_code_left
+ - new_line_code = left[:line_code]
- %td.new_line{id: new_line_code, class: "#{new_line_class}", data: { linenumber: line_number_right }}
- = link_to raw(line_number_right), "##{new_line_code}", id: new_line_code
+ %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }}
+ = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code
- if @comments_allowed && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(line_code_right, 'new')
- %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", "line_code" => new_line_code}= raw line_content_right
+ = link_to_new_diff_note(right[:line_code], 'new')
+ %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text])
- if @reply_allowed
- - comments_left, comments_right = organize_comments(type_left, type_right, line_code_left, line_code_right)
+ - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code])
- if comments_left.present? || comments_right.present?
= render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 977ca423f75..9a8208202e4 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -3,9 +3,11 @@
.suppressed-container
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
-%table.text-file{class: "#{'hide' if too_big}"}
+%table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' }
+
- last_line = 0
- - diff_file.diff_lines.each_with_index do |line, index|
+ - 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)
@@ -14,23 +16,27 @@
- 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
+ %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{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}", "line_code" => line_code}= raw diff_line_content(line.text)
+ %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)
- if @reply_allowed
- comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at)
- unless comments.empty?
- = render "projects/notes/diff_notes_with_reply", notes: comments, line: line.text
+ = render "projects/notes/diff_notes_with_reply", notes: comments, line: raw_diff_lines[index].text
- if last_line > 0
- = render "projects/diffs/match_line", {line: "",
- line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file}
+ = render "projects/diffs/match_line", { line: "",
+ line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file }
- if diff_file.diff.blank? && diff_file.mode_changed?
.file-mode-changed
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index f99bc9a85eb..15536c17f8e 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -3,17 +3,16 @@
Too many changes to show.
.pull-right
- unless diff_hard_limit_enabled?
- = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm btn-warning"
+ = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm"
- if current_controller?(:commit) or current_controller?(:merge_requests)
- if current_controller?(:commit)
- = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-sm"
- = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-sm"
+ = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm"
+ = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-sm"
- elsif @merge_request && @merge_request.persisted?
- = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-warning btn-sm"
- = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-sm"
+ = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-sm"
+ = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm"
%p
To preserve performance only
- %strong #{shown_files_count} of #{diffs.size}
+ %strong #{diff_files.count} of #{diff_files.real_size}
files are displayed.
-
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 650629ef1b9..6d872cd0b21 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,6 +1,4 @@
-- @blank_container = true
-
-.project-edit-container
+.project-edit-container.prepend-top-default
.project-edit-errors
.project-edit-content
.panel.panel-default
@@ -86,6 +84,8 @@
%br
%span.descr Share code pastes with others out of git repository
+ = render 'builds_settings', f: f
+
%fieldset.features
%legend
Project avatar:
@@ -112,61 +112,6 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- %fieldset.features
- %legend
- Continuous Integration
- .form-group
- .col-sm-offset-2.col-sm-10
- %p Get recent application code using the following command:
- .radio
- = f.label :build_allow_git_fetch do
- = f.radio_button :build_allow_git_fetch, 'false'
- %strong git clone
- %br
- %span.descr Slower but makes sure you have a clean dir before every build
- .radio
- = f.label :build_allow_git_fetch do
- = f.radio_button :build_allow_git_fetch, 'true'
- %strong git fetch
- %br
- %span.descr Faster
- .form-group
- = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
- .col-sm-10
- = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
- %p.help-block per build in minutes
- .form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
- .col-sm-10
- .input-group
- %span.input-group-addon /
- = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
- %span.input-group-addon /
- %p.help-block
- We will use this regular expression to find test coverage output in build trace.
- Leave blank if you want to disable this feature
- .bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
- %ul
- %li
- Simplecov (Ruby) -
- %code \(\d+.\d+\%\) covered
- %li
- pytest-cov (Python) -
- %code \d+\%\s*$
- %li
- phpunit --coverage-text --colors=never (PHP) -
- %code ^\s*Lines:\s*\d+.\d+\%
-
-
- %fieldset.features
- %legend
- Advanced settings
- .form-group
- = f.label :runners_token, "CI token", class: 'control-label'
- .col-sm-10
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used to checkout project.
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
@@ -174,6 +119,19 @@
.danger-settings
+ .panel.panel-default
+ .panel-heading Housekeeping
+ .errors-holder
+ .panel-body
+ %p
+ Runs a number of housekeeping tasks within the current repository,
+ such as compressing file revisions and removing unreachable objects.
+ %br
+
+ .form-actions
+ = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
+ method: :post, class: "btn btn-default"
+
- if can? current_user, :archive_project, @project
- if @project.archived?
.panel.panel-success
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 503d156661e..6ad7b05155a 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
= content_for :flash_message do
- if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh'
@@ -16,41 +18,42 @@
= link_to "adding README", new_readme_path, class: 'underlined-link'
file to this project.
-- if can?(current_user, :download_code, @project)
- .prepend-top-20
- .empty_wrapper
- %h3.page-title-empty
- Command line instructions
- %div.git-empty
- %fieldset
- %h5 Git global setup
- %pre.light-well
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+- if can?(current_user, :push_code, @project)
+ %div{ class: container_class }
+ .prepend-top-20
+ .empty_wrapper
+ %h3.page-title-empty
+ Command line instructions
+ %div.git-empty
+ %fieldset
+ %h5 Git global setup
+ %pre.light-well
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.light-well
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- git push -u origin master
+ %fieldset
+ %h5 Create a new repository
+ %pre.light-well
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ git push -u origin master
- %fieldset
- %h5 Existing folder or Git repository
- %pre.light-well
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git add .
- git commit
- git push -u origin master
+ %fieldset
+ %h5 Existing folder or Git repository
+ %pre.light-well
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git add .
+ git commit
+ git push -u origin master
- - if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
new file mode 100644
index 00000000000..905f6bbbd48
--- /dev/null
+++ b/app/views/projects/find_file/show.html.haml
@@ -0,0 +1,27 @@
+- page_title "Find File", @ref
+- header_title project_title(@project, "Files", project_files_path(@project))
+
+.file-finder-holder.tree-holder.clearfix
+ .gray-content-block.top-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'find_file', path: @path
+ %ul.breadcrumb.repo-breadcrumb
+ %li
+ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
+ = @project.path
+ %li.file-finder
+ %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path', autocomplete: 'off'}
+
+ %div.tree-content-holder
+ .table-holder
+ %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %tbody
+ = spinner nil, true
+
+:javascript
+ var projectFindFile = new ProjectFindFile($(".file-finder-holder"), {
+ url: "#{escape_javascript(namespace_project_files_path(@project.namespace, @project, @ref, @options.merge(format: :json)))}",
+ treeUrl: "#{escape_javascript(namespace_project_tree_path(@project.namespace, @project, @ref))}",
+ blobUrlTemplate: "#{escape_javascript(namespace_project_blob_path(@project.namespace, @project, @id || @commit.id))}"
+ });
+ new ShortcutsFindFile(projectFindFile);
diff --git a/app/views/projects/forks/_projects.html.haml b/app/views/projects/forks/_projects.html.haml
new file mode 100644
index 00000000000..2946e6dcbd0
--- /dev/null
+++ b/app/views/projects/forks/_projects.html.haml
@@ -0,0 +1,2 @@
+= render 'shared/projects/list', projects: projects, use_creator_avatar: true,
+ forks: true, show_last_commit_as_description: true
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
new file mode 100644
index 00000000000..4bcf2d9d533
--- /dev/null
+++ b/app/views/projects/forks/index.html.haml
@@ -0,0 +1,48 @@
+.top-area
+ .nav-text
+ - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
+ == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
+
+ .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: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short',
+ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
+
+ .dropdown
+ %button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light sort:
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_recently_created
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
+ = link_to page_filter_path(sort: sort_value_recently_created, without: excluded_filters) do
+ = sort_title_recently_created
+ = link_to page_filter_path(sort: sort_value_oldest_created, without: excluded_filters) do
+ = sort_title_oldest_created
+ = link_to page_filter_path(sort: sort_value_recently_updated, without: excluded_filters) do
+ = sort_title_recently_updated
+ = link_to page_filter_path(sort: sort_value_oldest_updated, without: excluded_filters) do
+ = sort_title_oldest_updated
+
+ - 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 btn-new' do
+ = icon('code-fork fw')
+ Fork
+ - else
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do
+ = icon('code-fork fw')
+ Fork
+
+
+= render 'projects', projects: @forks
+
+- if @private_forks_count > 0
+ .private-forks-notice
+ = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
+ %strong= pluralize(@private_forks_count, 'private fork')
+ %span you have no access to.
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 8a2c027a455..edabc2d3b44 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -22,7 +22,7 @@
- else
.fork-thumbnail
- = link_to namespace_project_fork_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/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
new file mode 100644
index 00000000000..c15386b4883
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -0,0 +1,58 @@
+%tr.generic_commit_status
+ %td.status
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
+ - else
+ = ci_status_with_icon(generic_commit_status.status)
+
+ %td.generic_commit_status-link
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = link_to generic_commit_status.target_url do
+ %strong ##{generic_commit_status.id}
+ - else
+ %strong ##{generic_commit_status.id}
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
+
+ %td
+ - if generic_commit_status.ref
+ = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if generic_commit_status.try(:runner)
+ = runner_link(generic_commit_status.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = generic_commit_status.stage
+
+ %td
+ = generic_commit_status.name
+
+ .pull-right
+ - if generic_commit_status.tags.any?
+ - generic_commit_status.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+
+ %td.duration
+ - if generic_commit_status.duration
+ #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
+
+ %td.timestamp
+ - if generic_commit_status.finished_at
+ %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if generic_commit_status.try(:coverage)
+ #{generic_commit_status.coverage}%
+
+ %td
diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml
deleted file mode 100644
index 87ac75a350f..00000000000
--- a/app/views/projects/go_import.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-!!! 5
-%html
- %head
- - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/')
- %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"}
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index a47643bd09c..79a56647c53 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
new file mode 100644
index 00000000000..13f5fc141fa
--- /dev/null
+++ b/app/views/projects/group_links/index.html.haml
@@ -0,0 +1,41 @@
+- page_title "Groups"
+%h3.page_title Share project with other groups
+%p.light
+ Projects can be stored in only one group at once. However you can share a project with other groups here.
+%hr
+- if @group_links.present?
+ .enabled-groups.panel.panel-default
+ .panel-heading
+ Already shared with
+ %ul.well-list
+ - @group_links.each do |group_link|
+ - group = group_link.group
+ %li
+ .pull-right
+ = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do
+ %i.icon-remove
+ disable sharing
+ = link_to group do
+ %strong
+ %i.icon-folder-open
+ = group.name
+ %br
+ .light up to #{group_link.human_access}
+
+
+.available-groups
+ %h4
+ Can be shared with
+ %div
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do
+ .form-group
+ = label_tag :link_group_id, 'Group', class: 'control-label'
+ .col-sm-10
+ = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
+ .form-group
+ = label_tag :link_group_access, 'Max access level', class: 'control-label'
+ .col-sm-10
+ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control"
+ .form-actions
+ = submit_tag "Share", class: "btn btn-create"
+
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index b18d9197d0b..67d016bd871 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -1,9 +1,9 @@
-- page_title "Web Hooks"
+- page_title "Webhooks"
%h3.page-title
- Web hooks
+ Webhooks
%p.light
- #{link_to "Web hooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be
+ #{link_to "Webhooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be
used for binding events when something is happening within the project.
%hr.clearfix
@@ -47,14 +47,14 @@
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
- This url will be triggered when an issue is created
+ This url will be triggered when an issue is created/updated/merged
%div
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
- This url will be triggered when a merge request is created
+ This url will be triggered when a merge request is created/updated/merged
%div
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
@@ -70,12 +70,12 @@
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
.form-actions
- = f.submit "Add Web Hook", class: "btn btn-create"
+ = f.submit "Add Webhook", class: "btn btn-create"
-if @hooks.any?
.panel.panel-default
.panel-heading
- Web hooks (#{@hooks.count})
+ Webhooks (#{@hooks.count})
%ul.well-list
- @hooks.each do |hook|
%li
diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml
index de415ae51a4..38469ed4774 100644
--- a/app/views/projects/issues/_closed_by_box.html.haml
+++ b/app/views/projects/issues/_closed_by_box.html.haml
@@ -1,2 +1,4 @@
-.issue-closed-by-widget.gray-content-block.second-block.white
- This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests), pipeline: :gfm)} is accepted.
+.issue-closed-by-widget.second-block
+ - pluralized_mr_this = merge_request_count > 1 ? "these" : "this"
+ - pluralized_mr_is = merge_request_count > 1 ? "are" : "is"
+ When #{pluralized_mr_this} merge #{"request".pluralize(merge_request_count)} #{pluralized_mr_is} accepted, this issue will be closed automatically.
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index dc434cf38c4..b151393abab 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,9 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- - if @issue.closed?
- = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
- - else
- = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close js-note-target-close', title: 'Close Issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-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, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 6588d9bdbe1..33c48199ba5 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
:javascript
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index f9cf4910df3..00e1a3d8069 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -5,7 +5,8 @@
.issue-title
%span.issue-title-text
- = link_to_gfm issue.title, issue_path(issue), class: "row_title"
+ = confidential_icon(issue)
+ = link_to_gfm issue.title, issue_path(issue), class: "title"
%ul.controls.light
- if issue.closed?
%li
@@ -15,6 +16,17 @@
%li
= link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
+ - upvotes, downvotes = issue.upvotes, issue.downvotes
+ - if upvotes > 0
+ %li
+ = icon('thumbs-up')
+ = upvotes
+
+ - if downvotes > 0
+ %li
+ = icon('thumbs-down')
+ = downvotes
+
- note_count = issue.notes.user.count
- if note_count > 0
%li
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index ca5b1a8386d..f34f3c05737 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -5,9 +5,4 @@
.nothing-here-block No issues to show
- if @issues.present?
- .issuable-filter-count
- %span.pull-right
- = @issues.total_count
- issues for this filter
-
= paginate @issues, theme: "gitlab"
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 254968e4f67..d6b38b327ff 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,7 +1,7 @@
--if @merge_requests.any?
+- if @merge_requests.any?
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
- %ul.bordered-list
+ %ul.unstyled-list
- has_any_ci = @merge_requests.any?(&:ci_commit)
- @merge_requests.each do |merge_request|
%li
@@ -11,7 +11,7 @@
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
- \##{merge_request.iid}
+ = merge_request.to_reference
%span.merge-request-info
%strong
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
@@ -24,3 +24,5 @@
MERGED
- elsif merge_request.closed?
CLOSED
+ - if @closed_by_merge_requests.present?
+ = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
new file mode 100644
index 00000000000..e66e4669d48
--- /dev/null
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -0,0 +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
+ = icon('code-fork')
+ New Branch
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
new file mode 100644
index 00000000000..b10cd03515f
--- /dev/null
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -0,0 +1,15 @@
+- if @related_branches.any?
+ %h2.related-branches-title
+ = pluralize(@related_branches.count, 'Related Branch')
+ %ul.unstyled-list
+ - @related_branches.each do |branch|
+ %li
+ - sha = @project.repository.find_branch(branch).target
+ - ci_commit = @project.ci_commit(sha) if sha
+ - if ci_commit
+ %span.related-branch-ci-status
+ = render_ci_status(ci_commit)
+ %span.related-branch-info
+ %strong
+ = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
+ = branch
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index dc8e477185b..ee8a9414657 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_issues_url(@project.namespace, @project)
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index d6260ab2900..fde9304c0f8 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -5,22 +5,19 @@
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
-.project-issuable-filter
- .controls
- .pull-left
- - if current_user
- .hidden-xs.pull-left
- = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
- %i.fa.fa-rss
-
+.top-area
+ = render 'shared/issuable/nav', type: :issues
+ .nav-controls
+ - 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')
= 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 pull-left", title: "New Issue", id: "new_issue_link" do
- %i.fa.fa-plus
+ = 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
+ = icon('plus')
New Issue
- = render 'shared/issuable/filter', type: :issues
+= render 'shared/issuable/filter', type: :issues
.issues-holder
= render "issues"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f931a0d3b92..ce5b84ee712 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -5,37 +5,54 @@
= render "header_title"
.issue
- .detail-page-header
- .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed
- .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open
- %span.identifier
- Issue ##{@issue.iid}
- %span.creator
- &middot;
- opened by #{link_to_member(@project, @issue.author, size: 24)}
- &middot;
- = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
- - if @issue.updated_at != @issue.created_at
- %span
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
+ .detail-page-header.issuable-header
+ .pull-left
+ .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"}
+ %span.hidden-xs
+ Closed
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon('check')
+ .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"}
+ %span.hidden-xs
+ Open
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon('circle-o')
- .pull-right
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
+ .issue-meta
+ = confidential_icon(@issue)
+ %strong.identifier
+ Issue ##{@issue.iid}
+ %span.creator
+ opened
+ .editor-details
+ .editor-details
+ = time_ago_with_tooltip(@issue.created_at)
+ by
+ %strong
+ = link_to_member(@project, @issue.author, avatar: false, 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",
+ by_username: true, avatar: false)
+
+ .pull-right.issue-btn-group
- if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
- New Issue
+ New issue
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen', 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_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 '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
+
.issue-details.issuable-details
- .detail-page-description.gray-content-block.second-block
+ .detail-page-description.content-block
%h2.title
= markdown escape_once(@issue.title), pipeline: :single_line
%div
@@ -46,22 +63,19 @@
= markdown(@issue.description, cache_key: [@issue, "description"])
%textarea.hidden.js-task-list-field
= @issue.description
+ = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
- .merge-requests
- = render 'merge_requests'
+ .merge-requests
+ = render 'merge_requests'
+ = render 'related_branches'
- .gray-content-block.second-block.oneline-block
+ .content-block.content-block-small
+ = render 'new_branch'
= render 'votes/votes_block', votable: @issue
- - if @closed_by_merge_requests.present?
- = render 'projects/issues/closed_by_box'
-
.row
- %section.col-md-9
+ %section.col-md-12
.issuable-discussion
= render 'projects/issues/discussion'
- %aside.col-md-3
- = render 'shared/issuable/sidebar', issuable: @issue
-
- = render 'shared/show_aside'
+= render 'shared/issuable/sidebar', issuable: @issue
diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml
index 2f0f3fcfb06..986d8c220db 100644
--- a/app/views/projects/issues/update.js.haml
+++ b/app/views/projects/issues/update.js.haml
@@ -1,3 +1,3 @@
-$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}");
-$('.issuable-sidebar').parent().effect('highlight')
-new Issue();
+$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}";
+$('aside.right-sidebar').effect('highlight');
+new IssuableContext();
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml
index 5ce2a7b985d..be7a0bb5628 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/projects/labels/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
-if @label.errors.any?
.row
.col-sm-offset-2.col-sm-10
@@ -10,7 +10,11 @@
.form-group
= f.label :title, class: 'control-label'
.col-sm-10
- = f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, class: "form-control", required: true, autofocus: true
+ .form-group
+ = f.label :description, class: 'control-label'
+ .col-sm-10
+ = f.text_field :description, class: "form-control js-quick-submit"
.form-group
= f.label :color, "Background color", class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index b70a9fc9fe5..4927d239c1e 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -1,10 +1,25 @@
%li{id: dom_id(label)}
- = link_to_label(label)
+ = render "shared/label_row", label: label
+
.pull-right
%strong.append-right-20
+ = link_to_label(label, type: :merge_request) do
+ = pluralize label.open_merge_requests_count, 'open merge request'
+
+ %strong.append-right-20
= link_to_label(label) do
= pluralize label.open_issues_count, 'open issue'
+ - if current_user
+ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
+ .subscription-status{data: {status: label_subscription_status(label)}}
+ %button.btn.btn-sm.btn-info.subscribe-button
+ %span= label_subscription_toggle_button_text(label)
+
- if can? current_user, :admin_label, @project
= link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm'
= link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
+
+- if current_user
+ :javascript
+ new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 9081bcfe9b3..cc41130a9dc 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,13 +1,14 @@
- page_title "Labels"
= render "header_title"
-.gray-content-block.top-block
- - if can? current_user, :admin_label, @project
- = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
- = icon('plus')
- New label
- .oneline
+.top-area
+ .nav-text
Labels can be applied to issues and merge requests.
+ .nav-controls
+ - if can? current_user, :admin_label, @project
+ = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
+ = icon('plus')
+ New label
.labels
- if @labels.present?
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index bff3c3b283d..393998f15b9 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,8 +1,8 @@
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- 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 close-mr-link js-note-target-close", title: "Close merge request"
+ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- 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 js-note-target-reopen", title: "Reopen merge request"
+ = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index a051729dc32..18cf3f14f0b 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,7 +1,7 @@
%li{ class: mr_css_classes(merge_request) }
.merge-request-title
%span.merge-request-title-text
- = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
+ = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "title"
%ul.controls.light
- if merge_request.merged?
%li
@@ -24,6 +24,17 @@
%li
= link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
+ - upvotes, downvotes = merge_request.upvotes, merge_request.downvotes
+ - if upvotes > 0
+ %li
+ = icon('thumbs-up')
+ = upvotes
+
+ - if downvotes > 0
+ %li
+ = icon('thumbs-down')
+ = downvotes
+
- note_count = merge_request.mr_and_commit_notes.user.count
- if note_count > 0
%li
@@ -37,7 +48,7 @@
= note_count
.merge-request-info
- \##{merge_request.iid} &middot;
+ #{merge_request.to_reference} &middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.target_project.default_branch != merge_request.target_branch
@@ -53,7 +64,7 @@
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
- = link_to_label(label, project: merge_request.project)
+ = link_to_label(label, project: merge_request.project, type: 'merge_request')
- if merge_request.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 0af970e4b92..5473fa19166 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -5,10 +5,5 @@
.nothing-here-block No merge requests to show
- if @merge_requests.present?
- .issuable-filter-count
- %span.pull-right
- = @merge_requests.total_count
- merge requests for this filter
-
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 236a545c840..01dc7519bee 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -33,23 +33,18 @@
%div= msg
- elsif @merge_request.source_branch.present? && @merge_request.target_branch.present?
- - if @merge_request.compare_failed
- .alert.alert-danger
- %h4 Compare failed
- %p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches.
- - else
- .light-well.append-bottom-default
- .center
- %h4
- There isn't anything to merge.
- %p.slead
- - if @merge_request.source_branch == @merge_request.target_branch
- You'll need to use different branch names to get a valid comparison.
- - else
- %span.label-branch #{@merge_request.source_branch}
- and
- %span.label-branch #{@merge_request.target_branch}
- are the same.
+ .light-well.append-bottom-default
+ .center
+ %h4
+ There isn't anything to merge.
+ %p.slead
+ - if @merge_request.source_branch == @merge_request.target_branch
+ You'll need to use different branch names to get a valid comparison.
+ - else
+ %span.label-branch #{@merge_request.source_branch}
+ and
+ %span.label-branch #{@merge_request.target_branch}
+ are the same.
.form-actions
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index a14943b15d3..9e59f7df71b 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -18,7 +18,7 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
@@ -31,22 +31,18 @@
%li.diffs-tab.active
= link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
- %span.badge= @diffs.size
+ %span.badge= @diffs.real_size
.tab-content
#commits.commits.tab-pane
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane.active
- - if @diffs.present?
- = render "projects/diffs/diffs", diffs: @diffs, project: @project
- - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
.alert.alert-danger
%h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
%p To preserve performance the line changes are not shown.
- else
- .alert.alert-danger
- %h4 This comparison includes a huge diff.
- %p To preserve performance the line changes are not shown.
+ = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs
- if @ci_commit
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index ba7c2c01e93..ee5b9fd95a8 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
@@ -34,18 +34,20 @@
%span into
= link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
= @merge_request.target_branch
+ - if @merge_request.open? && @merge_request.diverged_from_target_branch?
+ %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
= render "projects/merge_requests/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml"
- - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user)
+ - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.light.prepend-top-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present?
- %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
@@ -62,20 +64,17 @@
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
- %span.badge= @merge_request.diffs.size
+ %span.badge= @merge_request.diff_size
.tab-content
#notes.notes.tab-pane.voting_notes
- .gray-content-block.second-block.oneline-block
+ .content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request
.row
- %section.col-md-9
+ %section.col-md-12
.issuable-discussion
= render "projects/merge_requests/discussion"
- %aside.col-md-3
- = render 'shared/issuable/sidebar', issuable: @merge_request
- = render 'shared/show_aside'
#commits.commits.tab-pane
- # This tab is always loaded via AJAX
@@ -87,6 +86,10 @@
.mr-loading-status
= spinner
+= render 'shared/issuable/sidebar', issuable: @merge_request
+- if @merge_request.can_be_reverted?
+ = render "projects/commit/revert", commit: @merge_request.merge_commit, title: @merge_request.title
+
:javascript
var merge_request;
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 086298e5af1..e56a44e0a79 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -2,15 +2,19 @@
= render "header_title"
= render 'projects/last_push'
-.project-issuable-filter
- .controls
+
+.top-area
+ = render 'shared/issuable/nav', type: :merge_requests
+ .nav-controls
= render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- - if can? current_user, :create_merge_request, @project
- .pull-left.hidden-xs
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do
- %i.fa.fa-plus
- New Merge Request
- = render 'shared/issuable/filter', type: :merge_requests
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
+ = icon('plus')
+ New Merge Request
+
+= render 'shared/issuable/filter', type: :merge_requests
+
.merge-requests-holder
= render 'merge_requests'
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index 7f904ec42a0..a8f09f855d4 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,4 +1,4 @@
-.gray-content-block.middle-block.oneline-block
+.content-block.oneline-block
= icon("sort-amount-desc")
Most recent commits displayed first
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index d9cfc3d7ae9..1b0bae86ad4 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,5 +1,6 @@
- if @merge_request_diff.collected?
- = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project
+ = render "projects/diffs/diffs", diffs: @merge_request.diffs(diff_options),
+ project: @merge_request.project, diff_refs: @merge_request.diff_refs
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
- else
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 877cc3d744b..0dbd159298e 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -45,6 +45,10 @@
- unless @merge_request.can_be_merged_by?(current_user)
%p
Note that pushing to GitLab requires write access to this repository.
+ %p
+ %strong Tip:
+ You can also checkout merge requests locally by
+ %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines
:javascript
$(function(){
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index 0f81e5e8914..a23bd8d18d0 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,4 +1,4 @@
-.detail-page-description.gray-content-block.second-block
+.detail-page-description.content-block
%h2.title
= markdown escape_once(@merge_request.title), pipeline: :single_line
@@ -10,3 +10,5 @@
= markdown(@merge_request.description, cache_key: [@merge_request, "description"])
%textarea.hidden.js-task-list-field
= @merge_request.description
+
+ = edited_time_ago_with_tooltip(@merge_request, placement: 'bottom')
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 fc6fb2a0d42..c6cbe8589ef 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,18 +1,28 @@
.detail-page-header
.status-box{ class: status_box_class(@merge_request) }
- = @merge_request.state_human_name
- %span.identifier
- Merge Request ##{@merge_request.iid}
- %span.creator
- &middot;
- opened by #{link_to_member(@project, @merge_request.author, size: 24)}
- &middot;
- = time_ago_with_tooltip(@merge_request.created_at)
- - if @merge_request.updated_at != @merge_request.created_at
- %span
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom')
+ %span.hidden-xs
+ = @merge_request.state_human_name
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon(@merge_request.state_icon_name)
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+ .issue-meta
+ %strong.identifier
+ %span.hidden-sm.hidden-md.hidden-lg
+ MR
+ %span.hidden-xs
+ Merge Request
+ !#{@merge_request.iid}
+ %span.creator
+ opened
+ .editor-details
+ = 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")
+ %strong
+ = link_to_member(@project, @merge_request.author, avatar: false, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
+ by_username: true, avatar: false)
.issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request)
diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml
index 93db65ddf79..9cce5660e1c 100644
--- a/app/views/projects/merge_requests/update.js.haml
+++ b/app/views/projects/merge_requests/update.js.haml
@@ -1,3 +1,3 @@
-$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}");
-$('.issuable-sidebar').parent().effect('highlight')
-merge_request = new MergeRequest();
+$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}";
+$('aside.right-sidebar').effect('highlight');
+new IssuableContext();
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index d1d602eecdc..3abae9f0bf6 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -8,20 +8,18 @@
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
%div
- if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
- The changes were merged into
- #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
- The source branch has been removed.
-
+ %p
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ The source branch has been removed.
+ = render 'projects/merge_requests/widget/merged_buttons'
- elsif @merge_request.can_remove_source_branch?(current_user)
.remove_source_branch_widget
%p
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
You can remove the source branch now.
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
- %i.fa.fa-times
- Remove Source Branch
-
+ = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true
.remove_source_branch_widget.failed.hide
%p
Failed to remove source branch '#{@merge_request.source_branch}'.
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
new file mode 100644
index 00000000000..85a3a6ba9e2
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -0,0 +1,11 @@
+- source_branch_exists = local_assigns.fetch(:source_branch_exists, false)
+- mr_can_be_reverted = @merge_request.can_be_reverted?
+
+- if source_branch_exists || mr_can_be_reverted
+ .btn-group
+ - if source_branch_exists
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do
+ = icon('trash-o')
+ Remove Source Branch
+ - if mr_can_be_reverted
+ = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index d9a1730a8bc..807833741af 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,6 +1,6 @@
- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil
-= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f|
+= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
.accept-merge-holder.clearfix.js-toggle-container
.clearfix
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 39aa2437e18..23f2bca7baf 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f|
-if @milestone.errors.any?
.alert.alert-danger
%ul
@@ -9,12 +9,12 @@
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/notes/hints'
.clearfix
.error-alert
diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml
deleted file mode 100644
index 133d802aaca..00000000000
--- a/app/views/projects/milestones/_issue.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) }
- .pull-right.assignee-icon
- - if issue.assignee
- = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: ''
- %span
- = link_to [@project.namespace.becomes(Namespace), @project, issue] do
- %span.cgray ##{issue.iid}
- = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title
-
diff --git a/app/views/projects/milestones/_issues.html.haml b/app/views/projects/milestones/_issues.html.haml
deleted file mode 100644
index 6e4df75a3df..00000000000
--- a/app/views/projects/milestones/_issues.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id }
- - issues.sort_by(&:position).each do |issue|
- = render 'issue', issue: issue
- %li.light.ui-sort-disabled Drag and drop available
diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml
deleted file mode 100644
index a1033607c5d..00000000000
--- a/app/views/projects/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) }
- %span.str-truncated
- = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title
- .pull-right.assignee-icon
- - if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/projects/milestones/_merge_requests.html.haml b/app/views/projects/milestones/_merge_requests.html.haml
deleted file mode 100644
index 00889a5eb24..00000000000
--- a/app/views/projects/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id }
- - merge_requests.sort_by(&:position).each do |merge_request|
- = render 'merge_request', merge_request: merge_request
- %li.light.ui-sort-disabled Drag and drop available
diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index d6a44c9f0a1..77b566db6b6 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -1,30 +1,5 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
-
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
- = pluralize milestone.issues.count, 'Issue'
- &middot;
- = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
- = pluralize milestone.merge_requests.count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
-
- .row
- .col-sm-6
- = render 'shared/milestone_expired', milestone: milestone
- .col-sm-6
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
- = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
- %i.fa.fa-trash-o
- Delete
+= render 'shared/milestones/milestone',
+ milestone_path: namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone),
+ issues_path: namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
+ merge_requests_path: namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
+ milestone: milestone
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 114b06457a5..abe567af1dd 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -2,17 +2,14 @@
= render "header_title"
-.project-issuable-filter
- .controls
- - if can?(current_user, :admin_milestone, @project)
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
- %i.fa.fa-plus
- New Milestone
-
+.top-area
= render 'shared/milestones_filter'
-.gray-content-block
- Milestone allows you to group issues and set due date for it
+ .nav-controls
+ - if can?(current_user, :admin_milestone, @project)
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do
+ = icon('plus')
+ New Milestone
.milestones
%ul.content-list
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 1670ea8741a..be63875ab34 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -20,19 +20,19 @@
.pull-right
- if can?(current_user, :admin_milestone, @project)
- if @milestone.active?
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped"
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
- else
- = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped"
+ = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
- = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do
- %i.fa.fa-trash-o
+ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr" do
+ = icon('trash-o')
Delete
- = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
+ = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
+ = icon('pencil-square-o')
Edit
-.detail-page-description.gray-content-block.second-block
+.detail-page-description.milestone-detail.second-block
%h2.title
= markdown escape_once(@milestone.title), pipeline: :single_line
%div
@@ -42,90 +42,9 @@
= preserve do
= markdown @milestone.description
-- if @milestone.issues.any? && @milestone.can_be_closed?
+- 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.
-.context.prepend-top-default
- %p.lead
- Progress:
- #{@milestone.closed_items_count} closed
- &ndash;
- #{@milestone.open_items_count} open
- &nbsp;
- %span.light #{@milestone.percent_complete}% complete
- %span.pull-right= @milestone.expires_at
- = milestone_progress_bar(@milestone)
-
-%ul.center-top-menu.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab' do
- Issues
- %span.badge= @issues.count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
- Merge Requests
- %span.badge= @merge_requests.count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @users.count
-
-.tab-content
- .tab-pane.active#tab-issues
- .gray-content-block.middle-block
- .pull-right
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
- %i.fa.fa-plus
- New Issue
- - if can?(current_user, :read_issue, @project)
- = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All issues in this milestone
-
- .row.prepend-top-default
- .col-md-4
- = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned')
- .col-md-4
- = render('issues', title: 'Ongoing Issues (open and assigned)', issues: @issues.opened.assigned, id: 'ongoing')
- .col-md-4
- = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed')
-
- .tab-pane#tab-merge-requests
- .gray-content-block.middle-block
- .pull-right
- - if can?(current_user, :read_merge_request, @project)
- = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All merge requests in this milestone
-
- .row.prepend-top-default
- .col-md-3
- = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned')
- .col-md-3
- = render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing')
- .col-md-3
- = render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.closed, id: 'closed')
- .col-md-3
- .panel.panel-primary
- .panel-heading Merged
- %ul.well-list
- - @merge_requests.merged.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
-
- .tab-pane#tab-participants
- .gray-content-block.middle-block
- .oneline
- All participants to this milestone
-
- %ul.bordered-list
- - @users.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
+= render 'shared/milestones/summary', milestone: @milestone, project: @project
+= render 'shared/milestones/tabs', milestone: @milestone
diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml
index c731baf0a65..11f9859a90f 100644
--- a/app/views/projects/notes/_diff_notes_with_reply.html.haml
+++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml
@@ -7,7 +7,7 @@
%i.fa.fa-comment
= notes.count
%td.notes_content
- %ul.notes{ rel: note.discussion_id }
+ %ul.notes{ data: { discussion_id: note.discussion_id } }
= render notes
.discussion-reply-holder
= link_to_reply_diff(note)
diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml
index c6726cbafa3..bb761ed2f94 100644
--- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml
+++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml
@@ -8,7 +8,7 @@
%i.fa.fa-comment
= notes_left.count
%td.notes_content.parallel.old
- %ul.notes{ rel: note1.discussion_id }
+ %ul.notes{ data: { discussion_id: note1.discussion_id } }
= render notes_left
.discussion-reply-holder
@@ -23,7 +23,7 @@
%i.fa.fa-comment
= notes_right.count
%td.notes_content.parallel.new
- %ul.notes{ rel: note2.discussion_id }
+ %ul.notes{ data: { discussion_id: note2.discussion_id } }
= render notes_right
.discussion-reply-holder
@@ -31,4 +31,3 @@
- else
%td.notes_line.new= ""
%td.notes_content.parallel.new= ""
-
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 3ccda1b381c..13e624764d9 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,10 +1,10 @@
.note-edit-form
- = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
+ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field js-quick-submit'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
= render 'projects/notes/hints'
.note-form-actions
- = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
- = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
+ = 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/_form.html.haml b/app/views/projects/notes/_form.html.haml
index acb6dc52a8e..f675f092da1 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
@@ -8,11 +8,12 @@
= f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-quick-submit'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text'
= render 'projects/notes/hints'
.error-alert
.note-form-actions.clearfix
- = f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
+ = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
= yield(:note_actions)
- %a.btn.btn-cancel.js-close-discussion-note-form Cancel
+ %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
+ Discard draft
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 922535e5c4a..2cf32e6093d 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,4 +1,4 @@
-%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: { discussion: note.discussion_id } }
+%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] }
.timeline-entry-inner
.timeline-icon
%a{href: user_path(note.author)}
@@ -27,20 +27,13 @@
%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')
- - if note.updated_at != note.created_at
- %span
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
- - if note.updated_by && note.updated_by != note.author
- by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
-
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"])
- if note_editable?(note)
= render 'projects/notes/edit_form', note: note
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note.attachment.url
.note-attachment
@@ -54,4 +47,3 @@
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred')
- .clear
diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml
index ca60dd239b2..62db86fb181 100644
--- a/app/views/projects/notes/_notes.html.haml
+++ b/app/views/projects/notes/_notes.html.haml
@@ -2,10 +2,14 @@
- @discussions.each do |discussion_notes|
- note = discussion_notes.first
- if note_for_main_target?(note)
+ - next if note.cross_reference_not_visible_for?(current_user)
+
= render discussion_notes
- else
= render 'projects/notes/discussion', discussion_notes: discussion_notes
- else
- @notes.each do |note|
- next unless note.author
+ - next if note.cross_reference_not_visible_for?(current_user)
+
= render note
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index eb378b42603..910eb6cf66e 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -5,6 +5,16 @@
.js-main-target-form
- if can? current_user, :create_note, @project
= render "projects/notes/form", view: diff_view
+- else
+ .disabled-comment-area
+ .disabled-profile
+ .disabled-comment
+ %span
+ Please
+ = link_to "register",new_user_session_path
+ or
+ = link_to "login",new_user_session_path
+ to post a comment
:javascript
var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml
index 6903fad4a0a..3da2f2060b8 100644
--- a/app/views/projects/notes/discussions/_commit.html.haml
+++ b/app/views/projects/notes/discussions/_commit.html.haml
@@ -20,8 +20,7 @@
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
- else
.panel.panel-default
- .notes{ rel: discussion_notes.first.discussion_id }
+ .notes{ data: { discussion_id: discussion_notes.first.discussion_id } }
= render discussion_notes
.discussion-reply-holder
= link_to_reply_diff(discussion_notes.first)
-
diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml
index 0301445b5b2..820e31ccd61 100644
--- a/app/views/projects/notes/discussions/_diff.html.haml
+++ b/app/views/projects/notes/discussions/_diff.html.haml
@@ -9,22 +9,22 @@
= diff.new_path
- if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
%span.file-mode= "#{diff.a_mode} → #{diff.b_mode}"
- .diff-content
+ .diff-content.code.js-syntax-highlight
%table
- note.truncated_diff_lines.each do |line|
- type = line.type
- line_code = generate_line_code(note.file_path, line)
%tr.line_holder{ id: line_code, class: "#{type}" }
- if type == "match"
- %td.old_line= "..."
- %td.new_line= "..."
- %td.line_content.matched= line.text
+ %td.old_line.diff-line-num= "..."
+ %td.new_line.diff-line-num= "..."
+ %td.line_content.match= line.text
- else
- %td.old_line
+ %td.old_line.diff-line-num
= raw(type == "new" ? "&nbsp;" : line.old_pos)
- %td.new_line
+ %td.new_line.diff-line-num
= raw(type == "old" ? "&nbsp;" : line.new_pos)
- %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text)
+ %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text)
- if line_code == note.line_code
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index 1c2458fa144..c53033e367c 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -5,7 +5,7 @@
%small
(#{members.count})
- if can?(current_user, :admin_group_member, @group)
- .pull-right
+ .controls
= link_to group_group_members_path(@group), class: 'btn' do
= icon('pencil-square-o')
Manage group members
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index d708b01a114..f0f3bb3c177 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -4,7 +4,7 @@
.col-sm-10
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
- Search for existing users or invite new ones using their email address.
+ Search for users by name, username, or email, or invite new ones using their email address.
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
new file mode 100644
index 00000000000..62888e41935
--- /dev/null
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -0,0 +1,21 @@
+- @project_group_links.each do |group_links|
+ - shared_group = group_links.group
+ - shared_group_users_count = group_links.group.group_members.count
+ .panel.panel-default
+ .panel-heading
+ Shared with
+ %strong #{shared_group.name}
+ group, members with
+ %strong #{group_links.human_access}
+ role (#{shared_group_users_count})
+ - if current_user.can?(:admin_group, shared_group)
+ .panel-head-actions
+ = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
+ %i.fa.fa-pencil-square-o
+ Edit group members
+ %ul.content-list
+ - shared_group.group_members.order('access_level DESC').limit(20).each do |member|
+ = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
+ - if shared_group_users_count > 20
+ %li
+ and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index ccddab13aaf..e8dce30425f 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -4,7 +4,7 @@
project members
%small
(#{members.count})
- .pull-right
+ .controls
= form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 29225a36364..ebcfc907ebb 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,13 +1,12 @@
- page_title "Members"
= render "header_title"
-- @blank_container = true
-.project-members-page
+.project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
Add new user to project
- .pull-right
+ .controls
= link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
Import members
.panel-body
@@ -19,3 +18,6 @@
- if @group
= render "group_members", members: @group_members
+
+ - if @project_group_links.any? && @project.allowed_to_share_with_group?
+ = render "shared_group_members"
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index db7f244d002..8ee2aef0e61 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -8,12 +8,9 @@
row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
-- if @logs.present?
+- if @more_log_url
:plain
- var current_url = location.href.replace(/\/?$/, '/');
- var log_url = "#{escape_javascript(@log_url)}".replace(/\/?$/, '/');
-
- if(current_url == log_url) {
+ if($('#tree-slider').length) {
// Load more commit logs for each file in tree
// if we still on the same page
var url = "#{escape_javascript(@more_log_url)}";
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index bc80f2f29ad..c4a3f06ee06 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -9,9 +9,9 @@
%strong #{@tag.name}
.prepend-top-default
- = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f|
+ = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/notes/hints'
.error-alert
.form-actions.prepend-top-default
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
index b9486a9b492..24658319060 100644
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ b/app/views/projects/repositories/_download_archive.html.haml
@@ -10,7 +10,7 @@
%span.caret
%span.sr-only
Select Archive Format
- %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %ul.col-xs-10.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/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 315afe4a764..2d5b9f43c24 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -1,5 +1,6 @@
- page_title "Runners"
-.light
+
+.light.prepend-top-default
%p
A 'runner' is a process which runs a build.
You can setup as many runners as you need.
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 15c49767556..9b3d3f069d9 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_url(@project.namespace, @project)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events[0].updated_at.xmlschema if @events[0]
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 7466a098e24..4310f038fc9 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
@@ -8,11 +10,10 @@
= render 'shared/no_password'
= render 'projects/last_push'
-
= render "home_panel"
.project-stats.gray-content-block.second-block
- %ul.nav.nav-pills
+ %ul.nav
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit')
@@ -57,26 +58,17 @@
= link_to add_contribution_guide_path(@project) do
Add Contribution guide
-- if @project.archived?
- .text-warning.center.prepend-top-20
- %p
- = icon("exclamation-triangle fw")
- Archived project! Repository is read-only
-
- if @repository.commit
.content-block.second-block.white
- = render 'projects/last_commit', commit: @repository.commit, project: @project
+ %div{ class: container_class }
+ = render 'projects/last_commit', commit: @repository.commit, project: @project
-%div{class: "project-show-#{default_project_view}"}
- = render default_project_view
+%div{ class: container_class }
+ - if @project.archived?
+ .text-warning.center.prepend-top-20
+ %p
+ = icon("exclamation-triangle fw")
+ Archived project! Repository is read-only
-- if current_user
- - access = user_max_access_in_project(current_user.id, @project)
- - if access
- .prepend-top-20.project-footer
- .gray-content-block.footer-block.center
- You have #{access} access to this project.
- - if @project.project_member_by_id(current_user)
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do
- Leave this project
+ %div{class: "project-show-#{default_project_view}"}
+ = render default_project_view
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 28b706c5c7e..399782273d3 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -3,7 +3,7 @@
%li
%div
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
- %strong
+ %span.item-title
= icon('tag')
= tag.name
- if tag.message.present?
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
new file mode 100644
index 00000000000..ffeacb5a004
--- /dev/null
+++ b/app/views/projects/tags/destroy.js.haml
@@ -0,0 +1,3 @@
+$('.js-totaltags-count').html("#{@repository.tags.size}");
+- if @repository.tags.empty?
+ $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 3a2f75fecaa..77c7c4d23de 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -10,7 +10,7 @@
New Tag
%hr
-= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-requires-input" do
+= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do
.form-group
= label_tag :tag_name, nil, class: 'control-label'
.col-sm-10
@@ -30,7 +30,7 @@
= label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control'
+ = render 'projects/zen', attr: :release_description, classes: 'description form-control'
= render 'projects/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index b594d4f1f27..8c7f93f93b6 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -18,7 +18,7 @@
= 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
- %strong= @tag.name
+ %span.item-title= @tag.name
- if @tag.message.present?
%span.light
&nbsp;
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 3c5edf4b033..baaa2caa6de 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,7 +1,7 @@
%article.file-holder.readme-holder
.file-title
= blob_icon readme.mode, readme.name
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, @path, readme.name)) do
%strong
= readme.name
.file-content.wiki
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 1927883513a..558e6146ae9 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,6 +1,6 @@
%div.tree-content-holder
.table-holder
- %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %table.table#tree-slider{class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th Name
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 3343288ad2b..3eb626e6dca 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -40,7 +40,7 @@
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('pencil fw')
@@ -49,7 +49,7 @@
- continue_params = { to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to upload a file again.",
notice_now: edit_in_new_fork_notice_now }
- - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('file fw')
@@ -58,7 +58,7 @@
- continue_params = { to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to create a new directory again.",
notice_now: edit_in_new_fork_notice_now }
- - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('folder fw')
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index ec14bd7f65a..91fb2a44594 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -3,15 +3,15 @@
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
-
= render 'projects/last_push'
-- if can? current_user, :download_code, @project
- .tree-download-holder
- = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true
+.tree-controls
+ = render 'projects/find_file_link'
+ - if can? current_user, :download_code, @project
+ = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
#tree-holder.tree-holder.clearfix
- .gray-content-block.top-block
+ .nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml
index e80dffc1ced..efe1e6f24c2 100644
--- a/app/views/projects/variables/show.html.haml
+++ b/app/views/projects/variables/show.html.haml
@@ -3,9 +3,11 @@
Secret Variables
%p.light
- These variables will be set to environment by the runner and will be hidden in the build log.
+ These variables will be set to environment by the runner.
%br
So you can use them for passwords, secret keys or whatever you want.
+ %br
+ The value of the variable can be visible in build log if explicitly asked to do so.
%hr
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 1d257818dcd..f0d1932e23c 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f|
-if @page.errors.any?
#error_explanation
.alert.alert-danger
@@ -15,7 +15,7 @@
= f.label :content, class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :content, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
= render 'projects/notes/hints'
.clearfix
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 29bf5d62abe..2b91b7e8f65 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,12 +1,11 @@
-%span.pull-right
- - if (@page && @page.persisted?)
- = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
- Page History
- - if can?(current_user, :create_wiki, @project)
- = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- - if can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
- = icon('trash')
- Delete
+- if (@page && @page.persisted?)
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ Page History
+ - if can?(current_user, :create_wiki, @project)
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ %i.fa.fa-pencil-square-o
+ Edit
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
+ = icon('trash')
+ Delete
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index e6e6ad5bc4b..a722fbc5352 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,13 +1,5 @@
-.project-issuable-filter
- .controls
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- %i.fa.fa-plus
- New Page
-
- = render 'projects/wikis/new'
-
- %ul.center-top-menu
+.top-area
+ %ul.nav-links
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
@@ -17,3 +9,11 @@
= nav_link(path: 'wikis#git_access') do
= link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
Git Access
+
+ .nav-controls
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ = icon('plus')
+ New Page
+
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index f0547e9c057..919daf0a7b2 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -5,12 +5,10 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title New Wiki Page
.modal-body
- = label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
- %p.hidden.text-danger{data: { error: "slug" }}
- The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and /
- %p.hint
- Please don't use spaces.
- .form-actions
- = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
+ %form.new-wiki-page
+ .form-group
+ = label_tag :new_wiki_path do
+ %span Page slug
+ = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
+ .form-actions
+ = button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 23f64fbbd10..4dd818c7f67 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,16 +1,20 @@
- page_title "Edit", @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- .pull-right
+
+.top-area
+ .nav-text
+ %strong
+ - if @page.persisted?
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ - else
+ = @page.title.capitalize
+ %span.light
+ &middot;
+ Edit Page
+
+ .nav-controls
= render 'main_links'
- %h3.page-title.oneline
- %span.light Edit Page
- - if @page.persisted?
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
- - else
- = @page.title
= render 'form'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 11c8c4f0eba..dd27ea2b11b 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -3,14 +3,12 @@
= render 'nav'
.gray-content-block
- .row
- .col-sm-6
- %h3.page-title.oneline
- Git access for
- %strong= @project_wiki.path_with_namespace
+ %span.oneline
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .col-sm-6
- = render "shared/clone_panel", project: @project_wiki
+ .pull-right
+ = render "shared/clone_panel", project: @project_wiki
.git-empty.prepend-top-default
%fieldset
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 4322146ce34..dcaddae2b04 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,11 +1,14 @@
- page_title "History", @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- %h3.page-title
- %span.light History for
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
+
+.top-area
+ .nav-text
+ %strong
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ %span.light
+ &middot;
+ History
.table-holder
%table.table
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index aae1ad69ad9..92b494a513c 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -2,15 +2,12 @@
= render "header_title"
= render 'nav'
-.gray-content-block
- All pages in this wiki are listed below.
-
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
- %h4
- = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
- %small (#{wiki_page.format})
- .pull-right
- %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
+ = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
+ %small (#{wiki_page.format})
+ .pull-right
+ %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
= paginate @wiki_pages, theme: 'gitlab'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 309d40f52bc..067fb7f8f54 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,17 +1,18 @@
- page_title @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- = render 'main_links'
- %h3.page-title.oneline
- = @page.title.capitalize
+.top-area
+ .nav-text
+ %strong= @page.title.capitalize
%span.wiki-last-edit-by
&middot;
last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ .nav-controls
+ = render 'main_links'
+
- if @page.historical?
.warning_message
This is an old version of this page.
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 481451edb23..2c3fca439f3 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,4 +1,4 @@
-%ul.nav.nav-tabs.search-filter
+%ul.nav-links.search-filter
- if @project
%li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index ec478a5963d..4ef544136a8 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -6,14 +6,21 @@
- else
Any
%b.caret
- %ul.dropdown-menu
- %li
- = link_to search_filter_path(group_id: nil) do
- Any
- - current_user.authorized_groups.sort_by(&:name).each do |group|
- %li
- = link_to search_filter_path(group_id: group.id, project_id: nil) do
- = group.name
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Filter results by group
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-content
+ %ul
+ %li
+ = link_to search_filter_path(group_id: nil), class: ("is-active" if !params[:group_id].present?) do
+ Any
+ %li.divider
+ - current_user.authorized_groups.sort_by(&:name).each do |group|
+ %li
+ = link_to search_filter_path(group_id: group.id, project_id: nil), class: ("is-active" if params[:group_id] == group.id.to_s) do
+ = group.name
.dropdown.inline.prepend-left-10.project-filter
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
@@ -23,11 +30,18 @@
- else
Any
%b.caret
- %ul.dropdown-menu
- %li
- = link_to search_filter_path(project_id: nil) do
- Any
- - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
- %li
- = link_to search_filter_path(project_id: project.id, group_id: nil) do
- = project.name_with_namespace
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Filter results by project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-content
+ %ul
+ %li
+ = link_to search_filter_path(project_id: nil), class: ("is-active" if !params[:project_id].present?) do
+ Any
+ %li.divider
+ - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
+ %li
+ = link_to search_filter_path(project_id: project.id, group_id: nil), class: ("is-active" if params[:project_id] == project.id.to_s) do
+ = project.name_with_namespace
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index 17b0981f073..a9dbc84da29 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -11,4 +11,4 @@
= button_tag 'Search', class: "btn btn-primary"
- unless params[:snippets].eql? 'true'
%br
- = render 'filter'
+ = render 'filter' if current_user
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 2a38c98dcfc..60df348891c 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,7 +1,7 @@
- if @search_results.empty?
= render partial: "search/results/empty"
- else
- %p.light
+ .gray-content-block
Search results for
%code
= @search_term
@@ -18,6 +18,8 @@
= render 'shared/projects/list', projects: @objects
- else
= render partial: "search/results/#{@scope.singularize}", collection: @objects
+
+ - if @scope != 'projects'
= paginate @objects, theme: 'gitlab'
:javascript
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/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 2efa616d664..faeb2b55c6f 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -6,7 +6,7 @@
- if merge_request.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(merge_request.description))
+ = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project }))
%span.light
#{merge_request.project.name_with_namespace}
.pull-right
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 9a4f9fb9485..c9b7bd154af 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -1,50 +1,52 @@
+- snippet_blob = chunk_snippet(snippet_blob, @search_term)
+- snippet = snippet_blob[:snippet_object]
+- snippet_chunks = snippet_blob[:snippet_chunks]
+
.search-result-row
%span
- = snippet_blob[:snippet_object].title
+ = snippet.title
by
- = link_to user_snippets_path(snippet_blob[:snippet_object].author) do
- = image_tag avatar_icon(snippet_blob[:snippet_object].author_email), class: "avatar avatar-inline s16", alt: ''
- = snippet_blob[:snippet_object].author_name
- %span.light #{time_ago_with_tooltip(snippet_blob[:snippet_object].created_at)}
+ = link_to user_snippets_path(snippet.author) do
+ = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: ''
+ = snippet.author_name
+ %span.light #{time_ago_with_tooltip(snippet.created_at)}
%h4.snippet-title
- - snippet_path = reliable_snippet_path(snippet_blob[:snippet_object])
+ - snippet_path = reliable_snippet_path(snippet)
= link_to snippet_path do
.file-holder
.file-title
%i.fa.fa-file
- %strong= snippet_blob[:snippet_object].file_name
- - if markup?(snippet_blob[:snippet_object].file_name)
+ %strong= snippet.file_name
+ - if markup?(snippet.file_name)
.file-content.wiki
- - snippet_blob[:snippet_chunks].each do |snippet|
- - unless snippet[:data].empty?
- = render_markup(snippet_blob[:snippet_object].file_name, snippet[:data])
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ = render_markup(snippet.file_name, chunk[:data])
- else
.file-content.code
.nothing-here-block Empty file
- else
- .file-content.code
- %div.highlighted-data{ class: user_color_scheme }
- .line-numbers
- - snippet_blob[:snippet_chunks].each do |snippet|
- - unless snippet[:data].empty?
- - snippet[:data].lines.to_a.size.times do |index|
- - offset = defined?(snippet[:start_line]) ? snippet[:start_line] : 1
- - i = index + offset
- = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}" do
- %i.fa.fa-link
- = i
- - unless snippet == snippet_blob[:snippet_chunks].last
+ .file-content.code.js-syntax-highlight
+ .line-numbers
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
+ - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
+ - i = index + offset
+ = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
+ %i.fa.fa-link
+ = i
+ - unless snippet == snippet_chunks.last
+ %a.diff-line-num
+ = "."
+ %pre.code
+ %code
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ = chunk[:data]
+ - unless chunk == snippet_chunks.last
%a
- = "."
- .highlight.term
- %pre
- %code
- - snippet_blob[:snippet_chunks].each do |snippet|
- - unless snippet[:data].empty?
- = snippet[:data]
- - unless snippet == snippet_blob[:snippet_chunks].last
- %a
- = "..."
- - else
- .file-content.code
- .nothing-here-block Empty file
+ = "..."
+ - else
+ .file-content.code
+ .nothing-here-block Empty file
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index f5859481d46..235106c4f74 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -2,9 +2,9 @@
.blob-result
.file-holder
.file-title
- = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.filename) do
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.basename) do
%i.fa.fa-file
%strong
- = wiki_blob.filename
+ = wiki_blob.basename
.file-content.code.term
= render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index f4f3dcfc29f..215dbb3909e 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -1,5 +1,7 @@
- page_title @search_term
-= render 'search/form'
+
+.prepend-top-default
+ = render 'search/form'
- if @search_term
= render 'search/category'
= render 'search/results'
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 687a59c270f..faf7e49ed29 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,7 +1,7 @@
- project = project || @project
-.git-clone-holder
- .btn-group.clone-options
+.git-clone-holder.input-group
+ .input-group-btn
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
%span
= default_clone_protocol.upcase
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 7c57924277e..7afbaeddee8 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -7,7 +7,7 @@
.max-width-marker
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]),
- class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder],
+ class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 8495774accc..c38d9313dba 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,4 @@
-.btn-group.btn-group-next.event-filter
+%ul.nav-links.event-filter
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index 2bc98983d67..57856031d6e 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,13 +1,12 @@
-.file-content.code.js-syntax-highlight{ class: user_color_scheme }
+.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
- - blob.data.lines.each_index do |index|
+ - blob.data.each_line.each_with_index do |_, index|
- offset = defined?(first_line_number) ? first_line_number : 1
- i = index + offset
-# We're not using `link_to` because it is too slow once we get to thousands of lines.
- %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
%i.fa.fa-link
= i
.blob-content{data: {blob_id: blob.id}}
- :preserve
- #{highlight(blob.name, blob.data)}
+ = highlight(blob.name, blob.data)
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 285af56ad73..627814bcfae 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -11,6 +11,6 @@
%li
If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
%li
- The import will time out after 4 minutes. For big repositories, use a clone/push combination.
+ The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
%li
To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 4b4c9e9eabe..8ff9d4c1c7f 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -8,7 +8,7 @@
.pull-right
= link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
- %ul.well-list.issues-list
+ %ul.content-list.issues-list
- group[1].each do |issue|
= render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab"
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
new file mode 100644
index 00000000000..8134b15d245
--- /dev/null
+++ b/app/views/shared/_label_row.html.haml
@@ -0,0 +1,4 @@
+%span.label-row
+ = link_to_label(label)
+ %span.prepend-left-10
+ = markdown(label.description, pipeline: :single_line)
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index da49c48acd3..b07f1c5603e 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,21 +1,9 @@
-<svg width="36px" height="36px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="tanuki-logo">
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
- <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)">
- <g id="Page-1" sketch:type="MSShapeGroup">
- <g id="Fill-1-+-Group-24">
- <g id="Group-24">
- <g id="Group">
- <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329" class="tanuki-shape"></path>
- </g>
- </g>
- </g>
- </g>
- </g>
- </g>
+<svg width="36" height="36" id="tanuki-logo">
+ <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
+ <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
+ <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
+ <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
+ <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
+ <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
+ <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
</svg>
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index be17a511b26..e74fc36c797 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -8,7 +8,7 @@
.pull-right
= link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
- %ul.well-list.mr-list
+ %ul.content-list.mr-list
- group[1].each do |merge_request|
= render 'projects/merge_requests/merge_request', merge_request: merge_request
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index cbdecda4fff..cf16c203f9c 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,11 +1,10 @@
-.milestones-filters
- %ul.center-top-menu
- %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
- = link_to milestones_filter_path(state: 'opened') do
- Open
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to milestones_filter_path(state: 'closed') do
- Closed
- %li{class: ("active" if params[:state] == 'all')}
- = link_to milestones_filter_path(state: 'all') do
- All
+%ul.nav-links
+ %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
+ = link_to milestones_filter_path(state: 'opened') do
+ Open
+ %li{class: ("active" if params[:state] == 'closed')}
+ = link_to milestones_filter_path(state: 'closed') do
+ Closed
+ %li{class: ("active" if params[:state] == 'all')}
+ = link_to milestones_filter_path(state: 'all') do
+ All
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index c4431d66927..1c58345278a 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,6 +1,6 @@
- if @projects.any?
- .prepend-left-10.new-project-item-select-holder
- = project_select_tag :project_path, class: "new-project-item-select", data: { include_groups: local_assigns[:include_groups] }
+ .prepend-left-10.project-item-select-holder
+ = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }
%a.btn.btn-new.new-project-item-select-button
= icon('plus')
= local_assigns[:label]
@@ -8,12 +8,12 @@
:javascript
$('.new-project-item-select-button').on('click', function() {
- $('.new-project-item-select').select2('open');
+ $('.project-item-select').select2('open');
});
var relativePath = '#{local_assigns[:path]}';
- $('.new-project-item-select').on('click', function() {
+ $('.project-item-select').on('click', function() {
window.location = $(this).val() + '/' + relativePath;
});
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index 089179e677a..bb5fff2d3bb 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,6 +1,6 @@
- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key?
.no-ssh-key-message.alert.alert-warning.hidden-xs
- You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path, class: 'alert-link'} to your profile
+ You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile
.pull-right
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link'
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index 960ff00b49d..f4eb8e491b9 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -1,4 +1,4 @@
-- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project?
+- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
.project-limit-message.alert.alert-warning.hidden-xs
You won't be able to create new projects because you have reached your project limit.
diff --git a/app/views/shared/_promo.html.haml b/app/views/shared/_promo.html.haml
index 3596aabe309..09edf4000d5 100644
--- a/app/views/shared/_promo.html.haml
+++ b/app/views/shared/_promo.html.haml
@@ -1,5 +1,5 @@
.gitlab-promo
= link_to 'Homepage', promo_url
- = link_to "Blog", promo_url + '/blog/'
- = link_to "@gitlab", "https://twitter.com/gitlab"
- = link_to "Requests", "http://feedback.gitlab.com/"
+ = link_to 'Blog', promo_url + '/blog/'
+ = link_to '@gitlab', 'https://twitter.com/gitlab'
+ = link_to 'Requests', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#feature-proposals'
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 28d6f421fea..5a60ff5a5da 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -50,7 +50,7 @@
= form.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
- This url will be triggered when an issue is created
+ This url will be triggered when an issue is created/updated/merged
- if @service.supported_events.include?("merge_request")
%div
= form.check_box :merge_requests_events, class: 'pull-left'
@@ -58,7 +58,7 @@
= form.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
- This url will be triggered when a merge request is created
+ This url will be triggered when a merge request is created/updated/merged
- if @service.supported_events.include?("build")
%div
= form.check_box :build_events, class: 'pull-left'
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index af3d35de325..e3a6a5a68b6 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,6 +1,6 @@
.dropdown.inline.prepend-left-10
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
+ %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
@@ -20,3 +20,7 @@
= sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later) do
= sort_title_milestone_later
+ = link_to page_filter_path(sort: sort_value_upvotes) do
+ = sort_title_upvotes
+ = link_to page_filter_path(sort: sort_value_downvotes) do
+ = sort_title_downvotes
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a54c5fa8c33..fb9a8db0889 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,5 +1,8 @@
- group_member = local_assigns[:group_member]
-%li
+- css_class = '' unless local_assigns[:css_class]
+- css_class += " no-description" if group.description.blank?
+
+%li.group-row{ class: css_class }
- if group_member
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
@@ -9,14 +12,23 @@
= 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
- = image_tag group_icon(group), class: "avatar s46 hidden-xs"
- = link_to group, class: 'group-name' do
- %strong= group.name
+ .stats
+ %span
+ = icon('home')
+ = number_with_delimiter(group.projects.count)
+
+ %span
+ = icon('users')
+ = number_with_delimiter(group.users.count)
+
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ = link_to group, class: 'group-name title' do
+ = group.name
- if group_member
as
%span #{group_member.human_access}
- %div.light
- #{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")}
-
+ - if group.description.present?
+ .description
+ = markdown(group.description, pipeline: :description)
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
new file mode 100644
index 00000000000..1aa7ed1f2eb
--- /dev/null
+++ b/app/views/shared/groups/_list.html.haml
@@ -0,0 +1,6 @@
+- if groups.any?
+ %ul.content-list
+ - groups.each_with_index do |group, i|
+ = render "shared/groups/group", group: group
+- else
+ %h3 No groups found
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index be06738eac9..dfdc84ba4cc 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,28 +1,5 @@
.issues-filters
- .issues-state-filters
- %ul.center-top-menu
- %li{class: ("active" if params[:state] == 'opened')}
- = link_to page_filter_path(state: 'opened') do
- #{state_filters_text_for(:opened, @project)}
-
- - if defined?(type) && type == :merge_requests
- %li{class: ("active" if params[:state] == 'merged')}
- = link_to page_filter_path(state: 'merged') do
- #{state_filters_text_for(:merged, @project)}
-
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed') do
- #{state_filters_text_for(:closed, @project)}
- - else
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed') do
- #{state_filters_text_for(:closed, @project)}
-
- %li{class: ("active" if params[:state] == 'all')}
- = link_to page_filter_path(state: 'all') do
- #{state_filters_text_for(:all, @project)}
-
- .issues-details-filters.gray-content-block
+ .issues-details-filters.gray-content-block.second-block
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do
- if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
.check-all-holder
@@ -30,22 +7,77 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- = users_select_tag(:author_id, selected: params[:author_id],
- placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ - if params[:author_id]
+ = 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" } })
.filter-item.inline
- = users_select_tag(:assignee_id, selected: params[:assignee_id],
- placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
+ - if params[:assignee_id]
+ = 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" } })
.filter-item.inline.milestone-filter
- = select_tag('milestone_title', projects_milestones_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Milestone'})
+ - 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
.filter-item.inline.labels-filter
- = select_tag('label_name', projects_labels_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Label'})
+ - 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')
.pull-right
= render 'shared/sort_dropdown'
@@ -54,18 +86,32 @@
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline
- = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" })
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "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
- = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", 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
- = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" })
+ = 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 } })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= button_tag "Update issues", class: "btn update_selected_issues btn-save"
+- if @label
+ .gray-content-block.second-block
+ = render "shared/label_row", label: @label
+
:javascript
new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 90dc0062481..9ef729e960c 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -9,7 +9,7 @@
= f.label :title, class: 'control-label'
.col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad js-gfm-input js-quick-submit', required: true
+ class: 'form-control pad js-gfm-input', required: true
- if issuable.is_a?(MergeRequest)
%p.help-block
@@ -25,10 +25,19 @@
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description,
- classes: 'description form-control js-quick-submit'
+ classes: 'description form-control'
= 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
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
new file mode 100644
index 00000000000..a6970b7eebb
--- /dev/null
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -0,0 +1,25 @@
+%ul.nav-links.issues-state-filters
+ - if defined?(type) && type == :merge_requests
+ - page_context_word = 'merge requests'
+ - else
+ - page_context_word = 'issues'
+ %li{class: ("active" if params[:state] == 'opened')}
+ = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do
+ #{state_filters_text_for(:opened, @project)}
+
+ - if defined?(type) && type == :merge_requests
+ %li{class: ("active" if params[:state] == 'merged')}
+ = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do
+ #{state_filters_text_for(:merged, @project)}
+
+ %li{class: ("active" if params[:state] == 'closed')}
+ = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do
+ #{state_filters_text_for(:closed, @project)}
+ - else
+ %li{class: ("active" if params[:state] == 'closed')}
+ = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do
+ #{state_filters_text_for(:closed, @project)}
+
+ %li{class: ("active" if params[:state] == 'all')}
+ = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do
+ #{state_filters_text_for(:all, @project)}
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index da6bacbb74a..f1d92ef48b2 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -1,5 +1,10 @@
.block.participants
- .title
+ .sidebar-collapsed-icon
+ = icon('users')
+ %span
+ = participants.count
+ .title.hide-collapsed
= pluralize participants.count, "participant"
- participants.each do |participant|
- = link_to_member(@project, participant, name: false, size: 24)
+ %span.hide-collapsed
+ = link_to_member(@project, participant, name: false, size: 24)
diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml
index 3a5ad00aa91..afad48499b7 100644
--- a/app/views/shared/issuable/_search_form.html.haml
+++ b/app/views/shared/issuable/_search_form.html.haml
@@ -1,9 +1,8 @@
-= form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do
- .append-right-10.hidden-xs.hidden-sm
- = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input', spellcheck: false }
- = hidden_field_tag :state, params['state']
- = hidden_field_tag :scope, params['scope']
- = hidden_field_tag :assignee_id, params['assignee_id']
- = hidden_field_tag :author_id, params['author_id']
- = hidden_field_tag :milestone_id, params['milestone_id']
- = hidden_field_tag :label_id, params['label_id']
+= form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do
+ = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false }
+ = hidden_field_tag :state, params['state']
+ = hidden_field_tag :scope, params['scope']
+ = hidden_field_tag :assignee_id, params['assignee_id']
+ = hidden_field_tag :author_id, params['author_id']
+ = hidden_field_tag :milestone_id, params['milestone_id']
+ = hidden_field_tag :label_id, params['label_id']
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 79c5cc7f40a..23b1ed1e51b 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,83 +1,128 @@
-.issuable-sidebar.issuable-affix
- = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
- .block.assignee
- .title
- %label
- Assignee
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value
- - if issuable.assignee
- %strong= link_to_member(@project, issuable.assignee, size: 24)
+%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
+ .issuable-sidebar
+ .block
+ %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
+ .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'
+ - else
+ %a.btn.btn-default.disabled{href: '#'}
+ Prev
+ - if next_issuable = next_issuable_for(issuable)
+ = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn'
- else
- .light None
+ %a.btn.btn-default.disabled{href: '#'}
+ Next
- .selectbox
- = 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)
+ = 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
+ - if issuable.assignee
+ = link_to_member_avatar(issuable.assignee, size: 24)
+ - else
+ = icon('user')
+ .title.hide-collapsed
+ %label
+ Assignee
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .pull-right
+ = link_to 'Edit', '#', class: 'edit-link'
+ .value.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')
+ - else
+ .light None
- .block.milestone
- .title
- %label
- Milestone
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value
- - 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
- - else
- .light None
- .selectbox
- = 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'
+ .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)
- - if issuable.project.labels.any?
- .block
- .title
- %label Labels
+ .block.milestone
+ .sidebar-collapsed-icon
+ = icon('clock-o')
+ %span
+ - if issuable.milestone
+ = issuable.milestone.title
+ - else
+ No
+ .title.hide-collapsed
+ %label
+ Milestone
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.pull-right
= link_to 'Edit', '#', class: 'edit-link'
- .value.issuable-show-labels
- - if issuable.labels.any?
- - issuable.labels.each do |label|
- = link_to_label(label)
+ .value.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
- else
.light None
- .selectbox
- = 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" }
+ .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'
- .block
- .title
- Cross-project reference
- .cross-project-reference
- %span#cross-project-reference
- = cross_project_reference(@project, issuable)
- = clipboard_button(clipboard_target: 'span#cross-project-reference')
+ - if issuable.project.labels.any?
+ .block.labels
+ .sidebar-collapsed-icon
+ = icon('tags')
+ %span
+ = issuable.labels.count
+ .title.hide-collapsed
+ %label Labels
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .pull-right
+ = link_to 'Edit', '#', class: 'edit-link'
+ .value.issuable-show-labels.hide-collapsed
+ - 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" }
- = render "shared/issuable/participants", participants: issuable.participants(current_user)
+ = 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
+ - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
+ %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
+ %span= subscribed ? 'Unsubscribe' : 'Subscribe'
+ .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
+ .unsubscribed{class: ( 'hidden' if subscribed )}
+ You're not receiving notifications from this thread.
+ .subscribed{class: ( 'hidden' unless subscribed )}
+ You're receiving notifications because you're subscribed to this thread.
- - if current_user
- - subscribed = issuable.subscribed?(current_user)
- .block.light
- .title
- %label.light Notifications
- - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-gray.subscribe-button{:type => 'button'}
- %span= subscribed ? 'Unsubscribe' : 'Subscribe'
- .subscription-status{data: {status: subscribtion_status}}
- .unsubscribed{class: ( 'hidden' if subscribed )}
- You're not receiving notifications from this thread.
- .subscribed{class: ( 'hidden' unless subscribed )}
- You're receiving notifications because you're subscribed to this thread.
+ - project_ref = cross_project_reference(@project, issuable)
+ .block.project-reference
+ .sidebar-collapsed-icon
+ = clipboard_button(clipboard_text: project_ref)
+ .cross-project-reference.hide-collapsed
+ %span
+ Reference:
+ %cite{title: project_ref}
+ = project_ref
+ = clipboard_button(clipboard_text: project_ref)
- :javascript
- new Subscription("#{toggle_subscription_path(issuable)}");
- new IssuableContext();
+ :javascript
+ new Subscription('.subscription');
+ new IssuableContext();
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
new file mode 100644
index 00000000000..85888096722
--- /dev/null
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -0,0 +1,27 @@
+-# @project is present when viewing Project's milestone
+- project = @project || issuable.project
+- assignee = issuable.assignee
+- issuable_type = issuable.class.table_name
+- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
+
+%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+ %span
+ - if show_project_name
+ %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
+ %span{ class: 'issuable-number' }>= issuable.to_reference
+
+ - issuable.labels.each do |label|
+ = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
+ - render_colored_label(label)
+
+ - 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
+ - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
new file mode 100644
index 00000000000..8619939dde7
--- /dev/null
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -0,0 +1,16 @@
+- show_counter = local_assigns.fetch(:show_counter, false)
+- primary = local_assigns.fetch(:primary, false)
+- panel_class = primary ? 'panel-primary' : 'panel-default'
+
+.panel{ class: panel_class }
+ .panel-heading
+ = title
+ - if show_counter
+ .pull-right= issuables.size
+
+ - class_prefix = dom_class(issuables).pluralize
+ %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
+ = render partial: 'shared/milestones/issuable',
+ collection: issuables.sort_by(&:position),
+ as: :issuable,
+ locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml
new file mode 100644
index 00000000000..a8db7f8a556
--- /dev/null
+++ b/app/views/shared/milestones/_issues_tab.html.haml
@@ -0,0 +1,10 @@
+- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
+ show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
+
+.row.prepend-top-default
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Ongoing Issues (open and assigned)', issuables: issues.opened.assigned, id: 'ongoing', show_counter: true)
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Completed Issues (closed)', issuables: issues.closed, id: 'closed', show_counter: true)
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
new file mode 100644
index 00000000000..ba27bafd1bc
--- /dev/null
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -0,0 +1,18 @@
+%ul.bordered-list.manage-labels-list
+ - labels.each do |label|
+ - options = { milestone_title: @milestone.title, label_name: label.title }
+
+ %li
+ %span.label-row
+ = link_to milestones_label_path(options) do
+ - render_colored_label(label)
+ %span.prepend-left-10
+ = markdown(label.description, pipeline: :single_line)
+
+ .pull-right
+ %strong.issues-count
+ = link_to milestones_label_path(options.merge(state: 'opened')) do
+ - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
+ %strong.issues-count
+ = link_to milestones_label_path(options.merge(state: 'closed')) do
+ - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml
new file mode 100644
index 00000000000..c29d8ee6737
--- /dev/null
+++ b/app/views/shared/milestones/_merge_requests_tab.haml
@@ -0,0 +1,12 @@
+- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
+ show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
+
+.row.prepend-top-default
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true)
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
new file mode 100644
index 00000000000..6b25745c554
--- /dev/null
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -0,0 +1,45 @@
+- dashboard = local_assigns[:dashboard]
+- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first)
+
+%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
+ .row
+ .col-sm-6
+ %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
+ .col-sm-6
+ .pull-right.light #{milestone.percent_complete(current_user)}% complete
+ .row
+ .col-sm-6
+ = 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)
+ - if milestone.is_a?(GlobalMilestone)
+ .row
+ .col-sm-6
+ .expiration= render('shared/milestone_expired', milestone: milestone)
+ .projects
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = dashboard ? milestone.project.name_with_namespace : milestone.project.name
+ - if @group
+ .col-sm-6
+ - if can?(current_user, :admin_milestones, @group)
+ - if milestone.closed?
+ = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
+ - else
+ = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
+
+ - if @project
+ .row
+ .col-sm-6= render('shared/milestone_expired', milestone: milestone)
+ .col-sm-6
+ - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
+ = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do
+ = icon('pencil-square-o')
+ Edit
+ \
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
+ = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
+ = icon('trash-o')
+ Delete
diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml
new file mode 100644
index 00000000000..67ae85ac276
--- /dev/null
+++ b/app/views/shared/milestones/_participants_tab.html.haml
@@ -0,0 +1,8 @@
+%ul.bordered-list
+ - users.each do |user|
+ %li
+ = link_to user, title: user.name, class: "darken" do
+ = image_tag avatar_icon(user, 32), class: "avatar s32"
+ %strong= truncate(user.name, lenght: 40)
+ %br
+ %small.cgray= user.username
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
new file mode 100644
index 00000000000..385c6596606
--- /dev/null
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -0,0 +1,28 @@
+- project = local_assigns[:project]
+
+.context.prepend-top-default
+ .milestone-summary
+ %h4 Progress
+ %strong= milestone.issues_visible_to_user(current_user).size
+ issues:
+ %span.milestone-stat
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
+ open and
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
+ closed
+ %span.milestone-stat
+ %strong== #{milestone.percent_complete(current_user)}%
+ complete
+
+ %span.milestone-stat
+ %span.remaining-days= milestone_remaining_days(milestone)
+ %span.pull-right.tab-issues-buttons
+ - if project && can?(current_user, :create_issue, project)
+ = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do
+ %i.fa.fa-plus
+ New Issue
+ = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped"
+ %span.pull-right.tab-merge-requests-buttons.hidden
+ = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped"
+
+ = milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
new file mode 100644
index 00000000000..2b6ce2d7e7a
--- /dev/null
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -0,0 +1,30 @@
+%ul.nav-links.no-top.no-bottom
+ %li.active
+ = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
+ Issues
+ %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
+ %span.badge= milestone.merge_requests.size
+ %li
+ = link_to '#tab-participants', 'data-toggle' => 'tab' do
+ Participants
+ %span.badge= milestone.participants.count
+ %li
+ = link_to '#tab-labels', 'data-toggle' => 'tab' do
+ Labels
+ %span.badge= milestone.labels.count
+
+- show_project_name = local_assigns.fetch(:show_project_name, false)
+- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
+
+.tab-content.milestone-content
+ .tab-pane.active#tab-issues
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ .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
+ = render 'shared/milestones/participants_tab', users: milestone.participants
+ .tab-pane#tab-labels
+ = render 'shared/milestones/labels_tab', labels: milestone.labels
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
new file mode 100644
index 00000000000..cab8743a077
--- /dev/null
+++ b/app/views/shared/milestones/_top.html.haml
@@ -0,0 +1,58 @@
+- page_title milestone.title, "Milestones"
+
+- group = local_assigns[:group]
+
+.detail-page-header
+ .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
+ - if milestone.closed?
+ Closed
+ - elsif milestone.expired?
+ Expired
+ - else
+ Open
+ %span.identifier
+ Milestone #{milestone.title}
+ - if milestone.expires_at
+ %span.creator
+ &middot;
+ = milestone.expires_at
+ - if group
+ .pull-right
+ - if can?(current_user, :admin_milestones, group)
+ - if milestone.active?
+ = link_to 'Close Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
+ - else
+ = link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+
+.detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(milestone.title), pipeline: :single_line
+
+- 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}
+
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th Project
+ %th Open issues
+ %th State
+ %th Due date
+ - milestone.milestones.each do |ms|
+ %tr
+ %td
+ - 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_visible_to_user(current_user).opened.count
+ %td
+ - if ms.closed?
+ Closed
+ - else
+ Open
+ %td
+ = ms.expires_at
+
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
new file mode 100644
index 00000000000..e7e04621ff4
--- /dev/null
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -0,0 +1,22 @@
+- @sort ||= sort_value_recently_updated
+- archived = params[:archived]
+.dropdown.inline
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ = projects_sort_options_hash[@sort]
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Sort by
+ - projects_sort_options_hash.each do |value, title|
+ %li
+ = link_to filter_projects_path(sort: value, archived: archived), class: ("is-active" if @sort == value) do
+ = title
+
+ %li.divider
+ %li
+ = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do
+ Hide archived projects
+ %li
+ = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do
+ Show archived projects
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index e5ffe1e29ae..2e08bb2ac08 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -1,21 +1,24 @@
- projects_limit = 20 unless local_assigns[:projects_limit]
- avatar = true unless local_assigns[:avatar] == false
+- use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true
- stars = true unless local_assigns[:stars] == false
+- forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
+- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
+- remote = false unless local_assigns[:remote] == true
-%ul.projects-list
- - projects.each_with_index do |project, i|
- - css_class = (i >= projects_limit) ? 'hide' : nil
- = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
- avatar: avatar, stars: stars, css_class: css_class, ci: ci
-
- - if projects.size > projects_limit
- %li.bottom.center
- .light
- #{projects_limit} of #{pluralize(projects.count, 'project')} displayed.
- = link_to '#', class: 'js-expand' do
- Show all
+.projects-list-holder
+ - if projects.any?
+ %ul.projects-list.content-list
+ - projects.each_with_index do |project, i|
+ - css_class = (i >= projects_limit) ? 'hide' : nil
+ = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
+ avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
+ forks: forks, show_last_commit_as_description: show_last_commit_as_description
+ = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
+ - else
+ .nothing-here-block No projects found
:javascript
- new ProjectsList();
+ ProjectsList.init();
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 86249851a82..97cfb76cdb0 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -1,16 +1,25 @@
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
+- forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- css_class = '' unless local_assigns[:css_class]
-- css_class += " no-description" unless project.description.present?
+- 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.push(ci_commit.status) if ci_commit
+
%li.project-row{ class: css_class }
- = cache [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] do
+ = cache(cache_key) do
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
- = project_icon(project, alt: '', class: 'avatar project-avatar s46')
- %span.project-full-name
+ - 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
@@ -18,15 +27,28 @@
%span.project-name.filter-title
= project.name
- .project-controls
- - if ci && !project.empty_repo? && project.commit
- - if ci_commit = project.ci_commit(project.commit.sha)
+ .controls
+ - if project.main_language
+ %span
+ = project.main_language
+ - if ci_commit
+ %span
= render_ci_status(ci_commit)
- &nbsp;
+ - if forks
+ %span
+ = icon('code-fork')
+ = project.forks_count
- if stars
%span
- %i.fa.fa-star
+ = icon('star')
= project.star_count
- - if project.description.present?
- .project-description
+ %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' },
+ title: "#{visibility_level_label(project.visibility_level)} - #{project_visibility_level_description(project.visibility_level)}"}
+ = visibility_level_icon(project.visibility_level, fw: false)
+ - if show_last_commit_as_description
+ .description
+ = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
+ class: "commit-row-message"
+ - elsif project.description.present?
+ .description
= markdown(project.description, pipeline: :description)
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index d26a99bb14c..773ce8ac240 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,10 +1,11 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
+ %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
+ = @snippet.data
.file-content.wiki
= render_markup(@snippet.file_name, @snippet.data)
- else
- .file-content.code
- = render 'shared/file_highlight', blob: @snippet
+ = render 'shared/file_highlight', blob: @snippet
- else
.file-content.code
.nothing-here-block Empty file
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index c6294caddc7..a316a085107 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,10 +1,12 @@
%li.snippet-row
+ = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
+
.snippet-title
- = link_to reliable_snippet_path(snippet) do
+ = link_to reliable_snippet_path(snippet), class: 'title' do
= truncate(snippet.title, length: 60)
- if snippet.private?
%span.label.label-gray
- %i.fa.fa-lock
+ = icon('lock')
private
%span.monospace.pull-right
= snippet.file_name
@@ -15,6 +17,5 @@
.snippet-info
= link_to user_snippets_path(snippet.author) do
- = image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: ''
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index 4a84348ac82..83f61ce4b07 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.center-top-menu
+%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml
index b7e0162e80d..b8d93e9ff45 100644
--- a/app/views/sherlock/transactions/_queries.html.haml
+++ b/app/views/sherlock/transactions/_queries.html.haml
@@ -8,7 +8,7 @@
%tr
%th= t('sherlock.time')
%th= t('sherlock.query')
- %td
+ %th
%tbody
- @transaction.sorted_queries.each do |query|
%tr
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index 3c8ffb06648..9d4b0b2724c 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.center-top-menu
+%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index d9aa4dd1d2e..80a3e731e1d 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,4 +1,4 @@
-%ul.bordered-list
+%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
- if @snippets.empty?
%li
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index 2fe5b7fac83..e9e466c6350 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html"
xml.id user_url(@user)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events[0].updated_at.xmlschema if @events[0]
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 0bca8177e14..bca816f22cb 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,120 +1,117 @@
- page_title @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
+- @no_container = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
= render 'shared/show_aside'
-.cover-block
- .avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
- = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
- .cover-title
- = @user.name
+.user-profile
+ .cover-block
+ .cover-controls
+ - if @user == current_user
+ = link_to profile_path, class: 'btn btn-gray' do
+ = icon('pencil')
+ - elsif current_user
+ %span.report-abuse
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
+ = icon('exclamation-circle')
+ - if current_user
+ &nbsp;
+ = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
+ = icon('rss')
+
+ .avatar-holder
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+ .cover-title
+ = @user.name
+
+ .cover-desc
+ %span.middle-dot-divider
+ @#{@user.username}
+ %span.middle-dot-divider
+ Member since #{@user.created_at.to_s(:medium)}
- .cover-desc
- %span
- @#{@user.username}.
- if @user.bio.present?
- %span
- #{@user.bio}.
- %span
- Member since #{@user.created_at.stamp("Aug 21, 2011")}
-
- .cover-desc
- - unless @user.public_email.blank?
- .profile-link-holder
- = link_to @user.public_email, "mailto:#{@user.public_email}"
- - unless @user.skype.blank?
- .profile-link-holder
- = link_to "skype:#{@user.skype}", title: "Skype" do
- = icon('skype')
- - unless @user.linkedin.blank?
- .profile-link-holder
- = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
- = icon('linkedin-square')
- - unless @user.twitter.blank?
- .profile-link-holder
- = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
- = icon('twitter-square')
- - unless @user.website_url.blank?
- .profile-link-holder
- = link_to @user.short_website_url, @user.full_website_url
- - unless @user.location.blank?
- .profile-link-holder
- = icon('map-marker')
- = @user.location
-
-
- .cover-controls
- - if @user == current_user
- = link_to profile_path, class: 'btn btn-gray' do
- = icon('pencil')
- - elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
- - if current_user
- &nbsp;
- = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
- = icon('rss')
-
-.gray-content-block.second-block
- .user-calendar
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
-
-
-%ul.center-top-menu.no-top.no-bottom.bottom-border.wide
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @groups.any?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
- Groups
- - if @contributed_projects.present?
- %li
- = link_to "#contributed", 'data-toggle' => 'tab' do
- Contributed projects
- - if @projects.present?
- %li
- = link_to "#personal", 'data-toggle' => 'tab' do
- Personal projects
-
-.tab-content
- .tab-pane.active#activity
- .content_list
- = spinner
-
- - if @groups.any?
- .tab-pane#groups
- %ul.content-list
- - @groups.each do |group|
- = render 'shared/groups/group', group: group
-
- - if @contributed_projects.present?
- .tab-pane#contributed
- .contributed-projects
- = render 'shared/projects/list',
- projects: @contributed_projects.sort_by(&:star_count).reverse,
- projects_limit: 5, stars: true, avatar: true
-
- - if @projects.present?
- .tab-pane#personal
- .personal-projects
- = render 'shared/projects/list',
- projects: @projects.sort_by(&:star_count).reverse,
- projects_limit: 10, stars: true, avatar: true
+ .cover-desc
+ %p.profile-user-bio
+ = @user.bio
+
+ .cover-desc
+ - unless @user.public_email.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
+ - unless @user.skype.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "skype:#{@user.skype}", title: "Skype" do
+ = icon('skype')
+ - unless @user.linkedin.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = icon('linkedin-square')
+ - unless @user.twitter.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = icon('twitter-square')
+ - unless @user.website_url.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.short_website_url, @user.full_website_url
+ - unless @user.location.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('map-marker')
+ = @user.location
+
+ %ul.nav-links.center.user-profile-nav
+ %li.activity-tab
+ = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ Activity
+ %li.groups-tab
+ = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
+ Groups
+ %li.contributed-tab
+ = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
+ Contributed projects
+ %li.projects-tab
+ = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
+ Personal projects
+
+ %div{ class: container_class }
+ .tab-content
+ #activity.tab-pane
+ .gray-content-block.white.second-block
+ %div{ class: container_class }
+ .user-calendar{data: {href: user_calendar_path}}
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities
+
+ .content_list{ data: {href: user_path} }
+ = spinner
+
+ #groups.tab-pane
+ - # This tab is always loaded via AJAX
+
+ #contributed.contributed-projects.tab-pane
+ - # This tab is always loaded via AJAX
+
+ #projects.tab-pane
+ - # This tab is always loaded via AJAX
+
+ .loading-status
+ = spinner
:javascript
- $(".user-calendar").load("#{user_calendar_path}");
+ var userProfile;
+
+ userProfile = new User({
+ action: "#{controller.action_name}"
+ });
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index ce0a0113403..20d2d5f317b 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,46 +1,28 @@
.awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
- .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
+ %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)
- .counter
+ %span.award-control-text.js-counter
= notes.count
- if current_user
- .awards-controls
- %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"}
- = icon('smile-o')
- .emoji-menu
- .emoji-menu-content
- = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- - AwardEmoji.emoji_by_category.each do |category, emojis|
- %h5= AwardEmoji::CATEGORIES[category]
- %ul
- - emojis.each do |emoji|
- %li
- = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
+ %div.award-menu-holder.js-award-holder
+ %a.btn.award-control.js-add-award{"href" => "#"}
+ = icon('smile-o', {class: "award-control-icon"})
+ = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
+ %span.award-control-text
+ Add
- if current_user
- :coffeescript
- post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"
- noteable_type = "#{votable.class.name.underscore}"
- noteable_id = "#{votable.id}"
- aliases = #{AwardEmoji.aliases.to_json}
+ :javascript
+ var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
+ var noteable_type = "#{votable.class.name.underscore}";
+ var noteable_id = "#{votable.id}";
+ var aliases = #{AwardEmoji.aliases.to_json};
window.awards_handler = new AwardsHandler(
post_emoji_url,
noteable_type,
noteable_id,
aliases
- )
-
- $(".awards").on "click", ".emoji-menu-content li", (e) ->
- emoji = $(this).find(".emoji-icon").data("emoji")
- awards_handler.addAward(emoji)
-
- $(".awards").on "click", ".award", (e) ->
- emoji = $(this).find(".icon").data("emoji")
- awards_handler.addAward(emoji)
-
- $(".award").tooltip()
-
- $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false})
+ );
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
new file mode 100644
index 00000000000..6ff361e4d80
--- /dev/null
+++ b/app/workers/delete_user_worker.rb
@@ -0,0 +1,10 @@
+class DeleteUserWorker
+ include Sidekiq::Worker
+
+ def perform(current_user_id, delete_user_id, options = {})
+ delete_user = User.find(delete_user_id)
+ current_user = User.find(current_user_id)
+
+ DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys)
+ end
+end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 2d44d8d4dc6..605ec4f04e5 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -141,7 +141,7 @@ class IrkerWorker
end
def files_count(commit)
- files = "#{commit.diffs.count} file"
+ files = "#{commit.diffs.real_size} file"
files += 's' if commit.diffs.count > 1
files
end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
new file mode 100644
index 00000000000..1b3232cd365
--- /dev/null
+++ b/app/workers/new_note_worker.rb
@@ -0,0 +1,12 @@
+class NewNoteWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(note_id, note_params)
+ note = Note.find(note_id)
+
+ NotificationService.new.new_note(note)
+ Notes::PostProcessService.new(note).execute
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 994b8e8ed38..3cc232ef1ae 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,6 +1,5 @@
class PostReceive
include Sidekiq::Worker
- include Gitlab::Identifier
sidekiq_options queue: :post_receive
@@ -11,51 +10,44 @@ class PostReceive
log("Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"")
end
- repo_path.gsub!(/\.git\z/, "")
- repo_path.gsub!(/\A\//, "")
+ post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
- project = Project.find_with_namespace(repo_path)
-
- if project.nil?
+ if post_received.project.nil?
log("Triggered hook for non-existing project with full path \"#{repo_path} \"")
return false
end
- changes = Base64.decode64(changes) unless changes.include?(" ")
- changes = utf8_encode_changes(changes)
- changes = changes.lines
+ if post_received.wiki?
+ # Nothing defined here yet.
+ elsif post_received.regular_project?
+ process_project_changes(post_received)
+ else
+ log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"")
+ false
+ end
+ end
- changes.each do |change|
+ def process_project_changes(post_received)
+ post_received.changes.each do |change|
oldrev, newrev, ref = change.strip.split(' ')
- @user ||= identify(identifier, project, newrev)
+ @user ||= post_received.identify(newrev)
unless @user
- log("Triggered hook for non-existing user \"#{identifier} \"")
+ log("Triggered hook for non-existing user \"#{post_received.identifier} \"")
return false
end
if Gitlab::Git.tag_ref?(ref)
- GitTagPushService.new.execute(project, @user, oldrev, newrev, ref)
+ GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref)
else
- GitPushService.new.execute(project, @user, oldrev, newrev, ref)
+ GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
end
end
- def utf8_encode_changes(changes)
- changes = changes.dup
-
- changes.force_encoding("UTF-8")
- return changes if changes.valid_encoding?
-
- # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON.
- detection = CharlockHolmes::EncodingDetector.detect(changes)
- return changes unless detection && detection[:encoding]
-
- CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8')
- end
-
+ private
+
def log(message)
Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
new file mode 100644
index 00000000000..d06e4480292
--- /dev/null
+++ b/app/workers/project_destroy_worker.rb
@@ -0,0 +1,17 @@
+class ProjectDestroyWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(project_id, user_id, params)
+ begin
+ project = Project.find(project_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+
+ user = User.find(user_id)
+
+ ::Projects::DestroyService.new(project, user, params).execute
+ end
+end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 2f991c52339..21d311579e3 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -27,6 +27,7 @@ class RepositoryForkWorker
return
end
+ project.repository.after_import
project.import_finish
end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index d18c0706b30..2937493c614 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -4,52 +4,21 @@ class RepositoryImportWorker
sidekiq_options queue: :gitlab_shell
- def perform(project_id)
- project = Project.find(project_id)
+ attr_accessor :project, :current_user
- if project.import_url == Project::UNKNOWN_IMPORT_URL
- # In this case, we only want to import issues, not a repository.
- unless project.create_repository
- project.update(import_error: "The repository could not be created.")
- project.import_fail
- return
- end
- else
- begin
- gitlab_shell.import_repository(project.path_with_namespace, project.import_url)
- rescue Gitlab::Shell::Error => e
- project.update(import_error: e.message)
- project.import_fail
- return
- end
- end
+ def perform(project_id)
+ @project = Project.find(project_id)
+ @current_user = @project.creator
- data_import_result =
- case project.import_type
- when 'github'
- Gitlab::GithubImport::Importer.new(project).execute
- when 'gitlab'
- Gitlab::GitlabImport::Importer.new(project).execute
- when 'bitbucket'
- Gitlab::BitbucketImport::Importer.new(project).execute
- when 'google_code'
- Gitlab::GoogleCodeImport::Importer.new(project).execute
- when 'fogbugz'
- Gitlab::FogbugzImport::Importer.new(project).execute
- else
- true
- end
+ result = Projects::ImportService.new(project, current_user).execute
- unless data_import_result
- project.update(import_error: "The remote issue data could not be imported.")
+ if result[:status] == :error
+ project.update(import_error: result[:message])
project.import_fail
return
end
- if project.import_type == 'bitbucket'
- Gitlab::BitbucketImport::KeyDeleter.new(project).execute
- end
-
+ project.repository.after_import
project.import_finish
end
end
diff --git a/bin/background_jobs b/bin/background_jobs
index 5c85fb339e6..1f67d732949 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -27,17 +27,17 @@ restart()
stop
fi
killall
- start_sidekiq -d -L $sidekiq_logfile
+ start_sidekiq -d -L $sidekiq_logfile >> $sidekiq_logfile 2>&1
}
start_no_deamonize()
{
- start_sidekiq
+ start_sidekiq >> $sidekiq_logfile 2>&1
}
start_sidekiq()
{
- bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
+ bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@"
}
load_ok()
@@ -66,6 +66,9 @@ case "$1" in
start_no_deamonize)
start_no_deamonize
;;
+ start_foreground)
+ start_sidekiq
+ ;;
restart)
restart
;;
diff --git a/bin/web b/bin/web
index 67f236eb0bb..03fe7a6354b 100755
--- a/bin/web
+++ b/bin/web
@@ -5,6 +5,7 @@ app_root=$(pwd)
unicorn_pidfile="$app_root/tmp/pids/unicorn.pid"
unicorn_config="$app_root/config/unicorn.rb"
+unicorn_cmd="bundle exec unicorn_rails -c $unicorn_config -E $RAILS_ENV"
get_unicorn_pid()
{
@@ -18,7 +19,12 @@ get_unicorn_pid()
start()
{
- bundle exec unicorn_rails -D -c $unicorn_config -E $RAILS_ENV
+ $unicorn_cmd -D
+}
+
+start_foreground()
+{
+ $unicorn_cmd
}
stop()
@@ -37,6 +43,9 @@ case "$1" in
start)
start
;;
+ start_foreground)
+ start_foreground
+ ;;
stop)
stop
;;
diff --git a/config.ru b/config.ru
index a2525c81361..065ce59932f 100644
--- a/config.ru
+++ b/config.ru
@@ -7,8 +7,11 @@ if defined?(Unicorn)
# Unicorn self-process killer
require 'unicorn/worker_killer'
+ min = (ENV['GITLAB_UNICORN_MEMORY_MIN'] || 300 * 1 << 20).to_i
+ max = (ENV['GITLAB_UNICORN_MEMORY_MAX'] || 350 * 1 << 20).to_i
+
# Max memory size (RSS) per worker
- use Unicorn::WorkerKiller::Oom, (200 * (1 << 20)), (250 * (1 << 20))
+ use Unicorn::WorkerKiller::Oom, min, max
end
end
diff --git a/config/application.rb b/config/application.rb
index d255ff0719f..2b103c4592d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -4,8 +4,11 @@ require 'rails/all'
require 'devise'
I18n.config.enforce_available_locales = false
Bundler.require(:default, Rails.env)
+require_relative '../lib/gitlab/redis_config'
module Gitlab
+ REDIS_CACHE_NAMESPACE = 'cache:gitlab'
+
class Application < Rails::Application
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
@@ -31,7 +34,7 @@ module Gitlab
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
- config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt)
+ config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables, :import_url)
# Enable escaping HTML in JSON.
config.active_support.escape_html_entities_in_json = true
@@ -43,8 +46,8 @@ module Gitlab
# Enable the asset pipeline
config.assets.enabled = true
- config.assets.paths << Emoji.images_path
- config.assets.precompile << "emoji/*.png"
+ config.assets.paths << Gemojione.index.images_path
+ config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
# Version of your assets, change this if you want to expire all your assets
@@ -52,20 +55,6 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb)
- # Relative url support
- # Uncomment and customize the last line to run in a non-root path
- # WARNING: We recommend creating a FQDN to host GitLab in a root path instead of this.
- # Note that following settings need to be changed for this to work.
- # 1) In your application.rb file: config.relative_url_root = "/gitlab"
- # 2) In your gitlab.yml file: relative_url_root: /gitlab
- # 3) In your unicorn.rb: ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab"
- # 4) In ../gitlab-shell/config.yml: gitlab_url: "http://127.0.0.1/gitlab"
- # 5) In lib/support/nginx/gitlab : do not use asset gzipping, remove block starting with "location ~ ^/(assets)/"
- #
- # To update the path, run: sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
- #
- # config.relative_url_root = "/gitlab"
-
config.middleware.use Rack::Attack
# Allow access to GitLab API from other domains
@@ -79,23 +68,8 @@ module Gitlab
end
end
- # Use Redis caching across all environments
- redis_config_file = Rails.root.join('config', 'resque.yml')
-
- redis_url_string = if File.exists?(redis_config_file)
- YAML.load_file(redis_config_file)[Rails.env]
- else
- "redis://localhost:6379"
- end
-
- # Redis::Store does not handle Unix sockets well, so let's do it for them
- redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(redis_url_string)
- redis_uri = URI.parse(redis_url_string)
- if redis_uri.scheme == 'unix'
- redis_config_hash[:path] = redis_uri.path
- end
-
- redis_config_hash[:namespace] = 'cache:gitlab'
+ redis_config_hash = Gitlab::RedisConfig.redis_store_options
+ redis_config_hash[:namespace] = REDIS_CACHE_NAMESPACE
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
config.cache_store = :redis_store, redis_config_hash
@@ -105,5 +79,9 @@ module Gitlab
# This is needed for gitlab-shell
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
+
+ config.generators do |g|
+ g.factory_girl false
+ end
end
end
diff --git a/config/database.yml.env b/config/database.yml.env
index b2ff23cb5ab..1e35651c9a6 100644
--- a/config/database.yml.env
+++ b/config/database.yml.env
@@ -1,9 +1,17 @@
<%= ENV['RAILS_ENV'] %>:
+ ## Connection information
+ # Please be aware that the DATABASE_URL environment variable will take
+ # precedence over the following 6 parameters. For more information, see
+ # doc/administration/environment_variables.md
adapter: <%= ENV['GITLAB_DATABASE_ADAPTER'] || 'postgresql' %>
- encoding: <%= ENV['GITLAB_DATABASE_ENCODING'] || 'unicode' %>
database: <%= ENV['GITLAB_DATABASE_DATABASE'] || "gitlab_#{ENV['RAILS_ENV']}" %>
- pool: <%= ENV['GITLAB_DATABASE_POOL'] || '10' %>
username: <%= ENV['GITLAB_DATABASE_USERNAME'] || 'root' %>
password: <%= ENV['GITLAB_DATABASE_PASSWORD'] || '' %>
host: <%= ENV['GITLAB_DATABASE_HOST'] || 'localhost' %>
port: <%= ENV['GITLAB_DATABASE_PORT'] || '5432' %>
+
+ ## Behavior information
+ # The following parameters will be used even if you're using the DATABASE_URL
+ # environment variable.
+ encoding: <%= ENV['GITLAB_DATABASE_ENCODING'] || 'unicode' %>
+ pool: <%= ENV['GITLAB_DATABASE_POOL'] || '10' %>
diff --git a/config/environments/development.rb b/config/environments/development.rb
index c22722c606b..689694a3480 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -16,6 +16,9 @@ Rails.application.configure do
# Print deprecation notices to the Rails logger
config.active_support.deprecation = :log
+ # Raise an error on page load if there are pending migrations
+ config.active_record.migration_error = :page_load
+
# Only use best-standards-support built into browsers
config.action_dispatch.best_standards_support = :builtin
@@ -34,6 +37,8 @@ Rails.application.configure do
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
# Open sent mails in browser
config.action_mailer.delivery_method = :letter_opener
+ # Don't make a mess when bootstrapping a development environment
+ config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1')
config.eager_load = false
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index d6842affa6c..f96ac6f9753 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -7,8 +7,6 @@ Rails.application.configure do
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = false
- config.cache_store = :null_store
-
# Configure static asset server for tests with Cache-Control for performance
config.serve_static_files = true
config.static_cache_control = "public, max-age=3600"
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 2d9f730c183..500b745f55e 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -38,8 +38,12 @@ production: &base
# Otherwise, ssh host will be set to the `host:` value above
# ssh_host: ssh.host_example.com
- # WARNING: See config/application.rb under "Relative url support" for the list of
- # other files that need to be changed for relative url support
+ # Relative URL support
+ # WARNING: We recommend using an FQDN to host GitLab in a root path instead
+ # of using a relative URL.
+ # Documentation: http://doc.gitlab.com/ce/install/relative_url.html
+ # Uncomment and customize the following line to run in a non-root path
+ #
# relative_url_root: /gitlab
# Uncomment and customize if you can't use the default user to run GitLab (default: 'git')
@@ -204,6 +208,11 @@ production: &base
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.
@@ -279,15 +288,22 @@ production: &base
# auto_sign_in_with_provider: saml
# CAUTION!
- # This allows users to login without having a user account first (default: false).
+ # This allows users to login without having a user account first. Define the allowed providers
+ # using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none.
# User accounts will be created automatically when authentication was successful.
- allow_single_sign_on: false
+ allow_single_sign_on: ["saml"]
+
# Locks down those users until they have been cleared by the admin (default: true).
block_auto_created_users: true
# Look up new users in LDAP servers. If a match is found (same uid), automatically
# link the omniauth identity with the LDAP account. (default: false)
auto_link_ldap_user: false
+ # Allow users with existing accounts to login and auto link their account via SAML
+ # login, without having to do a manual login first and manually add SAML
+ # (default: false)
+ auto_link_saml_user: false
+
## Auth providers
# Uncomment the following lines and fill in the data of the auth provider you want to use
# If your favorite auth provider is not listed you can use others:
@@ -341,6 +357,12 @@ production: &base
# crowd_server_url: 'CROWD SERVER URL',
# application_name: 'YOUR_APP_NAME',
# application_password: 'YOUR_APP_PASSWORD' } }
+ #
+ # - { name: 'auth0',
+ # args: {
+ # client_id: 'YOUR_AUTH0_CLIENT_ID',
+ # client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
+ # namespace: 'YOUR_AUTH0_DOMAIN' } }
# SSO maximum session duration in seconds. Defaults to CAS default of 8 hours.
# cas3:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4fbd84ee890..626268d7648 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -9,13 +9,8 @@ class Settings < Settingslogic
gitlab.port.to_i == (gitlab.https ? 443 : 80)
end
- # get host without www, thanks to http://stackoverflow.com/a/6674363/1233435
- def get_host_without_www(url)
- url = URI.encode(url)
- uri = URI.parse(url)
- uri = URI.parse("http://#{url}") if uri.scheme.nil?
- host = uri.host.downcase
- host.start_with?('www.') ? host[4..-1] : host
+ def host_without_www(url)
+ host(url).sub('www.', '')
end
def build_gitlab_ci_url
@@ -87,6 +82,17 @@ class Settings < Settingslogic
custom_port
]
end
+
+ # Extract the host part of the given +url+.
+ def host(url)
+ url = url.downcase
+ url = "http://#{url}" unless url.start_with?('http')
+
+ # Get rid of the path so that we don't even have to encode it
+ url_without_path = url.sub(%r{(https?://[^\/]+)/?.*}, '\1')
+
+ URI.parse(url_without_path).host
+ end
end
end
@@ -108,6 +114,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
Settings.ldap['servers'].each do |key, server|
server['label'] ||= 'LDAP'
+ server['timeout'] ||= 10.seconds
server['block_auto_created_users'] = false if server['block_auto_created_users'].nil?
server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil?
server['active_directory'] = true if server['active_directory'].nil?
@@ -124,6 +131,7 @@ Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['au
Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil?
Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil?
Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil?
+Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil?
Settings.omniauth['providers'] ||= []
Settings.omniauth['cas3'] ||= Settingslogic.new({})
@@ -169,7 +177,7 @@ Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].
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]*-\d*))+)' if Settings.gitlab['issue_closing_pattern'].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?
Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10
@@ -199,11 +207,7 @@ Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_c
# Reply by email
#
Settings['incoming_email'] ||= Settingslogic.new({})
-Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
-Settings.incoming_email['port'] = 143 if Settings.incoming_email['port'].nil?
-Settings.incoming_email['ssl'] = false if Settings.incoming_email['ssl'].nil?
-Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_tls'].nil?
-Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil?
+Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
#
# Build Artifacts
@@ -227,7 +231,7 @@ Settings['gravatar'] ||= Settingslogic.new({})
Settings.gravatar['enabled'] = true if Settings.gravatar['enabled'].nil?
Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
-Settings.gravatar['host'] = Settings.get_host_without_www(Settings.gravatar['plain_url'])
+Settings.gravatar['host'] = Settings.host_without_www(Settings.gravatar['plain_url'])
#
# Cron Jobs
diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb
index 35b150c9929..bd74f90e7d2 100644
--- a/config/initializers/2_app.rb
+++ b/config/initializers/2_app.rb
@@ -3,6 +3,6 @@ module Gitlab
Settings
end
- VERSION = File.read(Rails.root.join("VERSION")).strip
- REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp
+ VERSION = File.read(Rails.root.join("VERSION")).strip.freeze
+ REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
end
diff --git a/config/initializers/date_time_formats.rb b/config/initializers/date_time_formats.rb
new file mode 100644
index 00000000000..57568203cab
--- /dev/null
+++ b/config/initializers/date_time_formats.rb
@@ -0,0 +1,9 @@
+# :short - 10 Nov
+# :medium - Nov 10, 2007
+# :long - November 10, 2007
+Date::DATE_FORMATS[:medium] = '%b %-d, %Y'
+
+# :short - 18 Jan 06:10
+# :medium - Jan 18, 2007 6:10am
+# :long - January 18, 2007 06:10
+Time::DATE_FORMATS[:medium] = '%b %-d, %Y %-I:%M%P'
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index d82cfb3ec0c..31dceaebcad 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -203,11 +203,11 @@ Devise.setup do |config|
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
- # config.warden do |manager|
- # manager.failure_app = AnotherApp
- # manager.intercept_401 = false
- # manager.default_strategies(scope: :user).unshift :some_external_strategy
- # end
+ config.warden do |manager|
+ manager.failure_app = Gitlab::DeviseFailure
+ # manager.intercept_401 = false
+ # manager.default_strategies(scope: :user).unshift :some_external_strategy
+ end
if Gitlab::LDAP::Config.enabled?
Gitlab.config.ldap.servers.values.each do |server|
diff --git a/config/initializers/go_get.rb b/config/initializers/go_get.rb
new file mode 100644
index 00000000000..7e7896b4900
--- /dev/null
+++ b/config/initializers/go_get.rb
@@ -0,0 +1 @@
+Rails.application.config.middleware.use(Gitlab::Middleware::Go)
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
new file mode 100644
index 00000000000..703f24f93b2
--- /dev/null
+++ b/config/initializers/gollum.rb
@@ -0,0 +1,13 @@
+module Gollum
+ GIT_ADAPTER = "rugged"
+end
+require "gollum-lib"
+
+module Gollum
+ class Committer
+ # Patch for UTF-8 path
+ def method_missing(name, *args)
+ index.send(name, *args)
+ end
+ end
+end
diff --git a/config/initializers/haml.rb b/config/initializers/haml.rb
index 7e8ddb3716b..1516476815a 100644
--- a/config/initializers/haml.rb
+++ b/config/initializers/haml.rb
@@ -1 +1,7 @@
Haml::Template.options[:ugly] = true
+
+# Remove the `:coffee` and `:coffeescript` filters
+#
+# See https://git.io/vztMu and http://stackoverflow.com/a/17571242/223897
+Haml::Filters.remove_filter('coffee')
+Haml::Filters.remove_filter('coffeescript')
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 2e4908192a1..3e1deb8d306 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -1,6 +1,5 @@
if Gitlab::Metrics.enabled?
require 'influxdb'
- require 'socket'
require 'connection_pool'
require 'method_source'
@@ -54,7 +53,26 @@ if Gitlab::Metrics.enabled?
Gitlab::Git.constants.each do |name|
const = Gitlab::Git.const_get(name)
- config.instrument_methods(const) if const.is_a?(Module)
+ next unless const.is_a?(Module)
+
+ config.instrument_methods(const)
+ config.instrument_instance_methods(const)
+ end
+
+ Dir[Rails.root.join('app', 'finders', '*.rb')].each do |path|
+ const = File.basename(path, '.rb').camelize.constantize
+
+ config.instrument_instance_methods(const)
+ end
+
+ [
+ :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
+ :Tag, :TagCollection, :Tree
+ ].each do |name|
+ const = Rugged.const_get(name)
+
+ config.instrument_methods(const)
+ config.instrument_instance_methods(const)
end
end
diff --git a/config/initializers/monkey_patch.rb b/config/initializers/monkey_patch.rb
new file mode 100644
index 00000000000..62b05a55285
--- /dev/null
+++ b/config/initializers/monkey_patch.rb
@@ -0,0 +1,48 @@
+## This patch is from rails 4.2-stable. Remove it when 4.2.6 is released
+## https://github.com/rails/rails/issues/21108
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractMysqlAdapter < AbstractAdapter
+ # SHOW VARIABLES LIKE 'name'
+ def show_variable(name)
+ variables = select_all("select @@#{name} as 'Value'", 'SCHEMA')
+ variables.first['Value'] unless variables.empty?
+ rescue ActiveRecord::StatementInvalid
+ nil
+ end
+
+
+ # MySQL is too stupid to create a temporary table for use subquery, so we have
+ # to give it some prompting in the form of a subsubquery. Ugh!
+ def subquery_for(key, select)
+ subsubselect = select.clone
+ subsubselect.projections = [key]
+
+ subselect = Arel::SelectManager.new(select.engine)
+ subselect.project Arel.sql(key.name)
+ # Materialized subquery by adding distinct
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
+ subselect.from subsubselect.distinct.as('__active_record_temp')
+ end
+ end
+ end
+end
+
+module ActiveRecord
+ module ConnectionAdapters
+ class MysqlAdapter < AbstractMysqlAdapter
+ ADAPTER_NAME = 'MySQL'.freeze
+
+ # Get the client encoding for this database
+ def client_encoding
+ return @client_encoding if @client_encoding
+
+ result = exec_query(
+ "select @@character_set_client",
+ 'SCHEMA')
+ @client_encoding = ENCODINGS[result.rows.last.last]
+ end
+ end
+ end
+end
diff --git a/config/initializers/mysql_ignore_postgresql_options.rb b/config/initializers/mysql_ignore_postgresql_options.rb
new file mode 100644
index 00000000000..835f3ec5574
--- /dev/null
+++ b/config/initializers/mysql_ignore_postgresql_options.rb
@@ -0,0 +1,49 @@
+# This patches ActiveRecord so indexes created using the MySQL adapter ignore
+# any PostgreSQL specific options (e.g. `using: :gin`).
+#
+# These patches do the following for MySQL:
+#
+# 1. Indexes created using the :opclasses option are ignored (as they serve no
+# purpose on MySQL).
+# 2. When creating an index with `using: :gin` the `using` option is discarded
+# as :gin is not a valid value for MySQL.
+# 3. The `:opclasses` option is stripped from add_index_options in case it's
+# used anywhere other than in the add_index methods.
+
+if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
+ module ActiveRecord
+ module ConnectionAdapters
+ class Mysql2Adapter < AbstractMysqlAdapter
+ alias_method :__gitlab_add_index, :add_index
+ alias_method :__gitlab_add_index_sql, :add_index_sql
+ alias_method :__gitlab_add_index_options, :add_index_options
+
+ def add_index(table_name, column_name, options = {})
+ unless options[:opclasses]
+ __gitlab_add_index(table_name, column_name, options)
+ end
+ end
+
+ def add_index_sql(table_name, column_name, options = {})
+ unless options[:opclasses]
+ __gitlab_add_index_sql(table_name, column_name, options)
+ end
+ end
+
+ def add_index_options(table_name, column_name, options = {})
+ if options[:using] and options[:using] == :gin
+ options = options.dup
+ options.delete(:using)
+ end
+
+ if options[:opclasses]
+ options = options.dup
+ options.delete(:opclasses)
+ end
+
+ __gitlab_add_index_options(table_name, column_name, options)
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb
new file mode 100644
index 00000000000..820cc89ef57
--- /dev/null
+++ b/config/initializers/postgresql_opclasses_support.rb
@@ -0,0 +1,188 @@
+# rubocop:disable all
+
+# These changes add support for PostgreSQL operator classes when creating
+# indexes and dumping/loading schemas. Taken from Rails pull request
+# https://github.com/rails/rails/pull/19090.
+#
+# License:
+#
+# Copyright (c) 2004-2016 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require 'date'
+require 'set'
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+# As the Struct definition is changed in this PR/patch we have to first remove
+# the existing one.
+ActiveRecord::ConnectionAdapters.send(:remove_const, :IndexDefinition)
+
+module ActiveRecord
+ module ConnectionAdapters #:nodoc:
+ # Abstract representation of an index definition on a table. Instances of
+ # this type are typically created and returned by methods in database
+ # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses) #:nodoc:
+ end
+ end
+end
+
+
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ module SchemaStatements
+ def add_index_options(table_name, column_name, options = {}) #:nodoc:
+ column_names = Array(column_name)
+ index_name = index_name(table_name, column: column_names)
+
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclasses)
+
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_type = options[:type].to_s if options.key?(:type)
+ index_name = options[:name].to_s if options.key?(:name)
+ max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
+
+ if options.key?(:algorithm)
+ algorithm = index_algorithms.fetch(options[:algorithm]) {
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ }
+ end
+
+ using = "USING #{options[:using]}" if options[:using].present?
+
+ if supports_partial_index?
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
+ end
+
+ if index_name.length > max_index_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
+ end
+ if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ end
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
+
+ [index_name, index_type, index_columns, index_options, algorithm, using]
+ end
+ end
+ end
+end
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module SchemaStatements
+ # Returns an array of indexes for the given table.
+ def indexes(table_name, name = nil)
+ result = query(<<-SQL, 'SCHEMA')
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
+ FROM pg_class t
+ INNER JOIN pg_index d ON t.oid = d.indrelid
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
+ WHERE i.relkind = 'i'
+ AND d.indisprimary = 'f'
+ AND t.relname = '#{table_name}'
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ ORDER BY i.relname
+ SQL
+
+ result.map do |row|
+ index_name = row[0]
+ unique = row[1] == 't'
+ indkey = row[2].split(" ")
+ inddef = row[3]
+ oid = row[4]
+
+ columns = Hash[query(<<-SQL, "SCHEMA")]
+ SELECT a.attnum, a.attname
+ FROM pg_attribute a
+ WHERE a.attrelid = #{oid}
+ AND a.attnum IN (#{indkey.join(",")})
+ SQL
+
+ column_names = columns.values_at(*indkey).compact
+
+ unless column_names.empty?
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
+ opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass|
+ column, opclass = column_and_opclass.split(' ').map(&:strip)
+ [column, opclass] if opclass
+ end.compact]
+
+ IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, opclasses)
+ end
+ end.compact
+ end
+
+ def add_index(table_name, column_name, options = {}) #:nodoc:
+ index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}"
+ end
+
+ protected
+
+ def quoted_columns_for_index(column_names, options = {})
+ column_opclasses = options[:opclasses] || {}
+ column_names.map {|name| "#{quote_column_name(name)} #{column_opclasses[name]}"}
+ end
+ end
+ end
+ end
+end
+
+module ActiveRecord
+ class SchemaDumper
+ private
+
+ def indexes(table, stream)
+ if (indexes = @connection.indexes(table)).any?
+ add_index_statements = indexes.map do |index|
+ statement_parts = [
+ "add_index #{remove_prefix_and_suffix(index.table).inspect}",
+ index.columns.inspect,
+ "name: #{index.name.inspect}",
+ ]
+ statement_parts << 'unique: true' if index.unique
+
+ index_lengths = (index.lengths || []).compact
+ statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
+
+ index_orders = index.orders || {}
+ statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
+ statement_parts << "where: #{index.where.inspect}" if index.where
+ statement_parts << "using: #{index.using.inspect}" if index.using
+ statement_parts << "type: #{index.type.inspect}" if index.type
+ statement_parts << "opclasses: #{index.opclasses}" if index.opclasses.present?
+
+ " #{statement_parts.join(', ')}"
+ end
+
+ stream.puts add_index_statements.sort.join("\n")
+ stream.puts
+ end
+ end
+ end
+end
diff --git a/config/initializers/relative_url.rb.sample b/config/initializers/relative_url.rb.sample
new file mode 100644
index 00000000000..125297d5385
--- /dev/null
+++ b/config/initializers/relative_url.rb.sample
@@ -0,0 +1,10 @@
+# Relative URL support
+# WARNING: We recommend using an FQDN to host GitLab in a root path instead
+# of using a relative URL.
+# Documentation: http://doc.gitlab.com/ce/install/relative_url.html
+# Copy this file to relative_url.rb and customize it to run in a non-root path
+#
+
+Rails.application.configure do
+ config.relative_url_root = "/gitlab"
+end
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
new file mode 100644
index 00000000000..e87899b2d5c
--- /dev/null
+++ b/config/initializers/sentry.rb
@@ -0,0 +1,20 @@
+# Be sure to restart your server when you modify this file.
+
+require 'gitlab/current_settings'
+include Gitlab::CurrentSettings
+
+if Rails.env.production?
+ # allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done
+ begin
+ sentry_enabled = current_application_settings.sentry_enabled
+ rescue
+ sentry_enabled = false
+ end
+
+ if sentry_enabled
+ Raven.configure do |config|
+ config.dsn = current_application_settings.sentry_dsn
+ config.release = Gitlab::REVISION
+ end
+ end
+end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 0fc725842ba..3da5d46be92 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -13,9 +13,12 @@ end
if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
+ redis_config = Gitlab::RedisConfig.redis_store_options
+ redis_config[:namespace] = 'session:gitlab'
+
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
- servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store
+ servers: redis_config,
key: '_gitlab_session',
secure: Gitlab.config.gitlab.https,
httponly: true,
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index dcf6ce74d96..cc83137745a 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,16 +1,9 @@
-# Custom Redis configuration
-config_file = Rails.root.join('config', 'resque.yml')
-
-resque_url = if File.exists?(config_file)
- YAML.load_file(config_file)[Rails.env]
- else
- "redis://localhost:6379"
- end
+SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab'
Sidekiq.configure_server do |config|
config.redis = {
- url: resque_url,
- namespace: 'resque:gitlab'
+ url: Gitlab::RedisConfig.url,
+ namespace: SIDEKIQ_REDIS_NAMESPACE
}
config.server_middleware do |chain|
@@ -36,7 +29,7 @@ end
Sidekiq.configure_client do |config|
config.redis = {
- url: resque_url,
- namespace: 'resque:gitlab'
+ url: Gitlab::RedisConfig.url,
+ namespace: SIDEKIQ_REDIS_NAMESPACE
}
end
diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample
index ec182502d4e..2287a76fca7 100644
--- a/config/initializers/smtp_settings.rb.sample
+++ b/config/initializers/smtp_settings.rb.sample
@@ -12,7 +12,7 @@ if Rails.env.production?
ActionMailer::Base.smtp_settings = {
address: "email.server.com",
- port: 456,
+ port: 465,
user_name: "smtp",
password: "123456",
domain: "gitlab.company.com",
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f6cfb5efd2a..cedb5e207bd 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -8,3 +8,7 @@ en:
wrong_size: "is the wrong size (should be %{file_size})"
size_too_small: "is too small (should be at least %{file_size})"
size_too_big: "is too big (should be at most %{file_size})"
+ views:
+ pagination:
+ previous: "Prev"
+ next: "Next"
diff --git a/config/mail_room.yml b/config/mail_room.yml
index 42f6f74c465..aed55f74eab 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -1,39 +1,47 @@
:mailboxes:
<%
-require_relative 'config/environment.rb'
-
-if Gitlab::IncomingEmail.enabled?
- config = Gitlab::IncomingEmail.config
-
- redis_config_file = "config/resque.yml"
- redis_url =
- if File.exists?(redis_config_file)
- YAML.load_file(redis_config_file)[Rails.env]
- else
- "redis://localhost:6379"
- end
- %>
- -
- :host: <%= config.host.to_json %>
- :port: <%= config.port.to_json %>
- :ssl: <%= config.ssl.to_json %>
- :start_tls: <%= config.start_tls.to_json %>
- :email: <%= config.user.to_json %>
- :password: <%= config.password.to_json %>
-
- :name: <%= config.mailbox.to_json %>
-
- :delete_after_delivery: true
-
- :delivery_method: sidekiq
- :delivery_options:
- :redis_url: <%= redis_url.to_json %>
- :namespace: resque:gitlab
- :queue: incoming_email
- :worker: EmailReceiverWorker
-
- :arbitration_method: redis
- :arbitration_options:
- :redis_url: <%= redis_url.to_json %>
- :namespace: mail_room:gitlab
+require "yaml"
+require "json"
+require_relative "lib/gitlab/redis_config"
+
+rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
+
+config_file = ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] || "config/gitlab.yml"
+if File.exists?(config_file)
+ all_config = YAML.load_file(config_file)[rails_env]
+
+ config = all_config["incoming_email"] || {}
+ config['enabled'] = false if config['enabled'].nil?
+ config['port'] = 143 if config['port'].nil?
+ config['ssl'] = false if config['ssl'].nil?
+ 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}')
+ redis_url = Gitlab::RedisConfig.new(rails_env).url
+ %>
+ -
+ :host: <%= config['host'].to_json %>
+ :port: <%= config['port'].to_json %>
+ :ssl: <%= config['ssl'].to_json %>
+ :start_tls: <%= config['start_tls'].to_json %>
+ :email: <%= config['user'].to_json %>
+ :password: <%= config['password'].to_json %>
+
+ :name: <%= config['mailbox'].to_json %>
+
+ :delete_after_delivery: true
+
+ :delivery_method: sidekiq
+ :delivery_options:
+ :redis_url: <%= redis_url.to_json %>
+ :namespace: resque:gitlab
+ :queue: incoming_email
+ :worker: EmailReceiverWorker
+
+ :arbitration_method: redis
+ :arbitration_options:
+ :redis_url: <%= redis_url.to_json %>
+ :namespace: mail_room:gitlab
+ <% end %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 3e7d9f78710..2ae282f48a6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -43,6 +43,8 @@ Rails.application.routes.draw do
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'
+ # Emojis
+ resources :emojis, only: :index
# Search
get 'search' => 'search#show'
@@ -52,9 +54,6 @@ Rails.application.routes.draw do
API::API.logger Rails.logger
mount API::API => '/api'
- # Get all keys of user
- get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ }
-
constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
constraints constraint do
mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
@@ -91,6 +90,12 @@ Rails.application.routes.draw do
end
end
+ resources :sent_notifications, only: [], constraints: { id: /\h{32}/ } do
+ member do
+ get :unsubscribe
+ end
+ end
+
# Spam reports
resources :abuse_reports, only: [:new, :create]
@@ -151,6 +156,11 @@ Rails.application.routes.draw do
to: "uploads#show",
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
+ # Appearance
+ get ":model/:mounted_as/:id/:filename",
+ to: "uploads#show",
+ constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
+
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show",
@@ -208,6 +218,8 @@ Rails.application.routes.draw do
end
resources :abuse_reports, only: [:index, :destroy]
+ resources :spam_logs, only: [:index, :destroy]
+
resources :applications
resources :groups, constraints: { id: /[^\/]+/ } do
@@ -222,7 +234,10 @@ Rails.application.routes.draw do
get :test
end
- resources :broadcast_messages, only: [:index, :create, :destroy]
+ resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
+ post :preview, on: :collection
+ end
+
resource :logs, only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
@@ -243,6 +258,14 @@ Rails.application.routes.draw do
end
end
+ resource :appearances, path: 'appearance' do
+ member do
+ get :preview
+ delete :logo
+ delete :header_logos
+ end
+ end
+
resource :application_settings, only: [:show, :update] do
resources :services
put :reset_runners_token
@@ -272,7 +295,7 @@ Rails.application.routes.draw do
resource :profile, only: [:show, :update] do
member do
get :audit_log
- get :applications
+ get :applications, to: 'oauth/applications#index'
put :reset_private_token
put :update_username
@@ -291,7 +314,7 @@ Rails.application.routes.draw do
end
end
resource :preferences, only: [:show, :update]
- resources :keys
+ resources :keys, except: [:new]
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
resource :two_factor_auth, only: [:new, :create, :destroy] do
@@ -309,6 +332,15 @@ Rails.application.routes.draw do
get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities,
constraints: { username: /.*/ }
+ get 'u/:username/groups' => 'users#groups', as: :user_groups,
+ constraints: { username: /.*/ }
+
+ get 'u/:username/projects' => 'users#projects', as: :user_projects,
+ constraints: { username: /.*/ }
+
+ get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects,
+ constraints: { username: /.*/ }
+
get '/u/:username' => 'users#show', as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
@@ -326,6 +358,12 @@ Rails.application.routes.draw do
resources :groups, only: [:index]
resources :snippets, only: [:index]
+ resources :todos, only: [:index, :destroy] do
+ collection do
+ delete :destroy_all
+ end
+ end
+
resources :projects, only: [:index] do
collection do
get :starred
@@ -344,6 +382,7 @@ Rails.application.routes.draw do
get :issues
get :merge_requests
get :projects
+ get :activity
end
scope module: :groups do
@@ -378,6 +417,7 @@ Rails.application.routes.draw do
delete :remove_fork
post :archive
post :unarchive
+ post :housekeeping
post :toggle_star
post :markdown_preview
get :autocomplete_sources
@@ -441,6 +481,24 @@ Rails.application.routes.draw do
end
scope do
+ get(
+ '/find_file/*id',
+ to: 'find_file#show',
+ constraints: { id: /.+/, format: /html/ },
+ as: :find_file
+ )
+ end
+
+ scope do
+ get(
+ '/files/*id',
+ to: 'find_file#list',
+ constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
+ as: :files
+ )
+ end
+
+ scope do
post(
'/create_dir/*id',
to: 'tree#create_dir',
@@ -468,12 +526,13 @@ Rails.application.routes.draw do
end
resource :avatar, only: [:show, :destroy]
- resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do
+ resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
get :builds
post :cancel_builds
post :retry_builds
+ post :revert
end
end
@@ -497,7 +556,7 @@ Rails.application.routes.draw do
end
end
- WIKI_SLUG_ID = { id: /[a-zA-Z.0-9_\-\/]+/ } unless defined? WIKI_SLUG_ID
+ WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
scope do
# Order matters to give priority to these matches
@@ -532,7 +591,7 @@ Rails.application.routes.draw do
end
end
- resource :fork, only: [:new, :create]
+ resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
resources :refs, only: [] do
@@ -580,7 +639,7 @@ Rails.application.routes.draw do
resource :variables, only: [:show, :update]
resources :triggers, only: [:index, :create, :destroy]
- resources :builds, only: [:index, :show] do
+ resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
end
@@ -588,8 +647,14 @@ Rails.application.routes.draw do
member do
get :status
post :cancel
- get :download
post :retry
+ post :erase
+ end
+
+ resource :artifacts, only: [] do
+ get :download
+ get :browse, path: 'browse(/*path)', format: false
+ get :file, path: 'file/*path', format: false
end
end
@@ -610,6 +675,10 @@ Rails.application.routes.draw do
collection do
post :generate
end
+
+ member do
+ post :toggle_subscription
+ end
end
resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do
@@ -636,6 +705,8 @@ Rails.application.routes.draw do
end
end
+ resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
+
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do
delete :delete_attachment
@@ -664,9 +735,18 @@ Rails.application.routes.draw do
end
resources :runner_projects, only: [:create, :destroy]
+ resources :badges, only: [], path: 'badges/*ref',
+ constraints: { ref: Gitlab::Regex.git_reference_regex } do
+ collection do
+ get :build, constraints: { format: /svg/ }
+ end
+ end
end
end
end
+ # Get all keys of user
+ get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ }
+
get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
end
diff --git a/config/sidekiq.yml.example b/config/sidekiq.yml.example
index c691db67c6c..714bc06cb24 100644
--- a/config/sidekiq.yml.example
+++ b/config/sidekiq.yml.example
@@ -1,2 +1,2 @@
---
+---
:concurrency: 5 \ No newline at end of file
diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example
index b937b092789..e5058cebce8 100644
--- a/config/unicorn.rb.example
+++ b/config/unicorn.rb.example
@@ -10,9 +10,12 @@
# Note: If you change this file in a Merge Request, please also create a
# Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-# WARNING: See config/application.rb under "Relative url support" for the list of
-# other files that need to be changed for relative url support
+
+# Relative URL support
+# WARNING: We recommend using an FQDN to host GitLab in a root path instead
+# of using a relative URL.
+# Documentation: http://doc.gitlab.com/ce/install/relative_url.html
+# Uncomment and customize the following line to run in a non-root path
#
# ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab"
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
new file mode 100644
index 00000000000..e3ca2b4eea3
--- /dev/null
+++ b/db/fixtures/development/14_builds.rb
@@ -0,0 +1,90 @@
+class Gitlab::Seeder::Builds
+ def initialize(project)
+ @project = project
+ end
+
+ def seed!
+ ci_commits.each do |ci_commit|
+ begin
+ build_create!(ci_commit, name: 'test build 1')
+ build_create!(ci_commit, status: 'success', name: 'test build 2')
+ print '.'
+ rescue ActiveRecord::RecordInvalid
+ print 'F'
+ end
+ end
+ end
+
+ def ci_commits
+ commits = @project.repository.commits('master', nil, 5)
+ commits_sha = commits.map { |commit| commit.raw.id }
+ commits_sha.map do |sha|
+ @project.ensure_ci_commit(sha)
+ end
+ rescue
+ []
+ end
+
+ def build_create!(ci_commit, opts = {})
+ attributes = build_attributes_for(ci_commit).merge(opts)
+ build = Ci::Build.new(attributes)
+
+ if %w(success failed).include?(build.status)
+ artifacts_cache_file(artifacts_archive_path) do |file|
+ build.artifacts_file = file
+ end
+
+ artifacts_cache_file(artifacts_metadata_path) do |file|
+ build.artifacts_metadata = file
+ end
+ end
+
+ build.save!
+
+ if %w(running success failed).include?(build.status)
+ # We need to set build trace after saving a build (id required)
+ build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
+ end
+ end
+
+ def build_attributes_for(ci_commit)
+ { name: 'test build', commands: "$ build command",
+ stage: 'test', stage_idx: 1, ref: 'master',
+ user_id: build_user, gl_project_id: @project.id,
+ status: build_status, commit_id: ci_commit.id,
+ created_at: Time.now, updated_at: Time.now }
+ end
+
+ def build_user
+ @project.team.users.sample
+ end
+
+ def build_status
+ Ci::Build::AVAILABLE_STATUSES.sample
+ end
+
+ def artifacts_archive_path
+ Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
+ end
+
+ def artifacts_metadata_path
+ Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+
+ end
+
+ def artifacts_cache_file(file_path)
+ cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
+
+ FileUtils.copy(file_path, cache_path)
+ File.open(cache_path) do |file|
+ yield file
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ Project.all.sample(10).each do |project|
+ project_builds = Gitlab::Seeder::Builds.new(project)
+ project_builds.seed!
+ end
+end
diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb
index b0c0b6450f6..78746c83225 100644
--- a/db/fixtures/production/001_admin.rb
+++ b/db/fixtures/production/001_admin.rb
@@ -1,31 +1,38 @@
+user_args = {
+ email: ENV['GITLAB_ROOT_EMAIL'].presence || 'admin@example.com',
+ name: 'Administrator',
+ username: 'root',
+ admin: true
+}
+
if ENV['GITLAB_ROOT_PASSWORD'].blank?
- password = '5iveL!fe'
- expire_time = Time.now
+ user_args[:password_automatically_set] = true
+ user_args[:force_random_password] = true
else
- password = ENV['GITLAB_ROOT_PASSWORD']
- expire_time = nil
+ user_args[:password] = ENV['GITLAB_ROOT_PASSWORD']
end
-admin = User.create(
- email: "admin@example.com",
- name: "Administrator",
- username: 'root',
- password: password,
- password_expires_at: expire_time,
- theme_id: Gitlab::Themes::APPLICATION_DEFAULT
+user = User.new(user_args)
+user.skip_confirmation!
-)
+if user.save
+ puts "Administrator account created:".green
+ puts
+ puts "login: root".green
-admin.projects_limit = 10000
-admin.admin = true
-admin.save!
-admin.confirm
-
-if admin.valid?
-puts %Q[
-Administrator account created:
+ if user_args.key?(:password)
+ puts "password: #{user_args[:password]}".green
+ else
+ puts "password: You'll be prompted to create one on your first visit.".green
+ end
+ puts
+else
+ puts "Could not create the default administrator account:".red
+ puts
+ user.errors.full_messages.map do |message|
+ puts "--> #{message}".red
+ end
+ puts
-login.........root
-password......#{password}
-]
+ exit 1
end
diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb
new file mode 100644
index 00000000000..395083f2a03
--- /dev/null
+++ b/db/migrate/20130711063759_create_project_group_links.rb
@@ -0,0 +1,10 @@
+class CreateProjectGroupLinks < ActiveRecord::Migration
+ def change
+ create_table :project_group_links do |t|
+ t.integer :project_id, null: false
+ t.integer :group_id, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb
new file mode 100644
index 00000000000..00e3947a6bb
--- /dev/null
+++ b/db/migrate/20130820102832_add_access_to_project_group_link.rb
@@ -0,0 +1,5 @@
+class AddAccessToProjectGroupLink < ActiveRecord::Migration
+ def change
+ add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
+ end
+end
diff --git a/db/migrate/20150930110012_add_group_share_lock.rb b/db/migrate/20150930110012_add_group_share_lock.rb
new file mode 100644
index 00000000000..78d1a4538f2
--- /dev/null
+++ b/db/migrate/20150930110012_add_group_share_lock.rb
@@ -0,0 +1,5 @@
+class AddGroupShareLock < ActiveRecord::Migration
+ def change
+ add_column :namespaces, :share_with_group_lock, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20151201203948_raise_hook_url_limit.rb b/db/migrate/20151201203948_raise_hook_url_limit.rb
new file mode 100644
index 00000000000..98a7fca6f6f
--- /dev/null
+++ b/db/migrate/20151201203948_raise_hook_url_limit.rb
@@ -0,0 +1,5 @@
+class RaiseHookUrlLimit < ActiveRecord::Migration
+ def change
+ change_column :web_hooks, :url, :string, limit: 2000
+ end
+end
diff --git a/db/migrate/20151228111122_remove_public_from_namespace.rb b/db/migrate/20151228111122_remove_public_from_namespace.rb
new file mode 100644
index 00000000000..f4c848bbf47
--- /dev/null
+++ b/db/migrate/20151228111122_remove_public_from_namespace.rb
@@ -0,0 +1,6 @@
+# Migration type: online
+class RemovePublicFromNamespace < ActiveRecord::Migration
+ def change
+ remove_column :namespaces, :public, :boolean
+ end
+end
diff --git a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
new file mode 100644
index 00000000000..6c282fc5039
--- /dev/null
+++ b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
@@ -0,0 +1,5 @@
+class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :artifacts_metadata, :text
+ end
+end
diff --git a/db/migrate/20151231152326_add_akismet_to_application_settings.rb b/db/migrate/20151231152326_add_akismet_to_application_settings.rb
new file mode 100644
index 00000000000..3f52c758f9a
--- /dev/null
+++ b/db/migrate/20151231152326_add_akismet_to_application_settings.rb
@@ -0,0 +1,8 @@
+class AddAkismetToApplicationSettings < ActiveRecord::Migration
+ def change
+ change_table :application_settings do |t|
+ t.boolean :akismet_enabled, default: false
+ t.string :akismet_api_key
+ end
+ end
+end
diff --git a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb
new file mode 100644
index 00000000000..78fdfeaf5cf
--- /dev/null
+++ b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb
@@ -0,0 +1,5 @@
+class RemoveAlertTypeFromBroadcastMessages < ActiveRecord::Migration
+ def change
+ remove_column :broadcast_messages, :alert_type, :integer
+ end
+end
diff --git a/db/migrate/20160106162223_add_index_milestones_title.rb b/db/migrate/20160106162223_add_index_milestones_title.rb
new file mode 100644
index 00000000000..767885e2aac
--- /dev/null
+++ b/db/migrate/20160106162223_add_index_milestones_title.rb
@@ -0,0 +1,5 @@
+class AddIndexMilestonesTitle < ActiveRecord::Migration
+ def change
+ add_index :milestones, :title
+ end
+end
diff --git a/db/migrate/20160106164438_remove_influxdb_credentials.rb b/db/migrate/20160106164438_remove_influxdb_credentials.rb
new file mode 100644
index 00000000000..47e74400b97
--- /dev/null
+++ b/db/migrate/20160106164438_remove_influxdb_credentials.rb
@@ -0,0 +1,6 @@
+class RemoveInfluxdbCredentials < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :metrics_username, :string
+ remove_column :application_settings, :metrics_password, :string
+ end
+end
diff --git a/db/migrate/20160109054846_create_spam_logs.rb b/db/migrate/20160109054846_create_spam_logs.rb
new file mode 100644
index 00000000000..f12fe9f8f78
--- /dev/null
+++ b/db/migrate/20160109054846_create_spam_logs.rb
@@ -0,0 +1,16 @@
+class CreateSpamLogs < ActiveRecord::Migration
+ def change
+ create_table :spam_logs do |t|
+ t.integer :user_id
+ t.string :source_ip
+ t.string :user_agent
+ t.boolean :via_api
+ t.integer :project_id
+ t.string :noteable_type
+ t.string :title
+ t.text :description
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160113111034_add_metrics_sample_interval.rb b/db/migrate/20160113111034_add_metrics_sample_interval.rb
new file mode 100644
index 00000000000..b741f5d2c75
--- /dev/null
+++ b/db/migrate/20160113111034_add_metrics_sample_interval.rb
@@ -0,0 +1,6 @@
+class AddMetricsSampleInterval < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :metrics_sample_interval, :integer,
+ default: 15
+ end
+end
diff --git a/db/migrate/20160118155830_add_sentry_to_application_settings.rb b/db/migrate/20160118155830_add_sentry_to_application_settings.rb
new file mode 100644
index 00000000000..fa7ff9d9228
--- /dev/null
+++ b/db/migrate/20160118155830_add_sentry_to_application_settings.rb
@@ -0,0 +1,8 @@
+class AddSentryToApplicationSettings < ActiveRecord::Migration
+ def change
+ change_table :application_settings do |t|
+ t.boolean :sentry_enabled, default: false
+ t.string :sentry_dsn
+ end
+ end
+end
diff --git a/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb
new file mode 100644
index 00000000000..26606b10b54
--- /dev/null
+++ b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb
@@ -0,0 +1,6 @@
+class AddIpBlockingSettingsToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :ip_blocking_enabled, :boolean, default: false
+ add_column :application_settings, :dnsbl_servers_list, :text
+ end
+end
diff --git a/db/migrate/20160119111158_add_services_category.rb b/db/migrate/20160119111158_add_services_category.rb
new file mode 100644
index 00000000000..a9110a8418b
--- /dev/null
+++ b/db/migrate/20160119111158_add_services_category.rb
@@ -0,0 +1,39 @@
+class AddServicesCategory < ActiveRecord::Migration
+ def up
+ add_column :services, :category, :string, default: 'common', null: false
+
+ category = quote_column_name('category')
+ type = quote_column_name('type')
+
+ execute <<-EOF
+UPDATE services
+SET #{category} = 'issue_tracker'
+WHERE #{type} IN (
+ 'CustomIssueTrackerService',
+ 'GitlabIssueTrackerService',
+ 'IssueTrackerService',
+ 'JiraService',
+ 'RedmineService'
+);
+EOF
+
+ execute <<-EOF
+UPDATE services
+SET #{category} = 'ci'
+WHERE #{type} IN (
+ 'BambooService',
+ 'BuildkiteService',
+ 'CiService',
+ 'DroneCiService',
+ 'GitlabCiService',
+ 'TeamcityService'
+);
+ EOF
+
+ add_index :services, :category
+ end
+
+ def down
+ remove_column :services, :category
+ end
+end
diff --git a/db/migrate/20160119112418_add_services_default.rb b/db/migrate/20160119112418_add_services_default.rb
new file mode 100644
index 00000000000..69a42d7b873
--- /dev/null
+++ b/db/migrate/20160119112418_add_services_default.rb
@@ -0,0 +1,20 @@
+class AddServicesDefault < ActiveRecord::Migration
+ def up
+ add_column :services, :default, :boolean, default: false
+
+ default = quote_column_name('default')
+ type = quote_column_name('type')
+
+ execute <<-EOF
+UPDATE services
+SET #{default} = true
+WHERE #{type} = 'GitlabIssueTrackerService'
+EOF
+
+ add_index :services, :default
+ end
+
+ def down
+ remove_column :services, :default
+ end
+end
diff --git a/db/migrate/20160119145451_add_ldap_email_to_users.rb b/db/migrate/20160119145451_add_ldap_email_to_users.rb
new file mode 100644
index 00000000000..654d31ab15a
--- /dev/null
+++ b/db/migrate/20160119145451_add_ldap_email_to_users.rb
@@ -0,0 +1,30 @@
+class AddLdapEmailToUsers < ActiveRecord::Migration
+ def up
+ add_column :users, :ldap_email, :boolean, default: false, null: false
+
+ if Gitlab::Database.mysql?
+ execute %{
+ UPDATE users, identities
+ SET users.ldap_email = TRUE
+ WHERE identities.user_id = users.id
+ AND users.email LIKE 'temp-email-for-oauth%'
+ AND identities.provider LIKE 'ldap%'
+ AND identities.extern_uid IS NOT NULL
+ }
+ else
+ execute %{
+ UPDATE users
+ SET ldap_email = TRUE
+ FROM identities
+ WHERE identities.user_id = users.id
+ AND users.email LIKE 'temp-email-for-oauth%'
+ AND identities.provider LIKE 'ldap%'
+ AND identities.extern_uid IS NOT NULL
+ }
+ end
+ end
+
+ def down
+ remove_column :users, :ldap_email
+ end
+end
diff --git a/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb b/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb
new file mode 100644
index 00000000000..d6c6aa4a4e8
--- /dev/null
+++ b/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb
@@ -0,0 +1,5 @@
+class AddBaseCommitShaToMergeRequestDiffs < ActiveRecord::Migration
+ def change
+ add_column :merge_request_diffs, :base_commit_sha, :string
+ end
+end
diff --git a/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb b/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb
new file mode 100644
index 00000000000..d50791410f9
--- /dev/null
+++ b/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddEmailAuthorInBodyToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :email_author_in_body, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20160122185421_add_pending_delete_to_project.rb b/db/migrate/20160122185421_add_pending_delete_to_project.rb
new file mode 100644
index 00000000000..046a5d8fc32
--- /dev/null
+++ b/db/migrate/20160122185421_add_pending_delete_to_project.rb
@@ -0,0 +1,5 @@
+class AddPendingDeleteToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :pending_delete, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb
new file mode 100644
index 00000000000..41821cdcc42
--- /dev/null
+++ b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb
@@ -0,0 +1,6 @@
+class RemoveIpBlockingSettingsFromApplicationSettings < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :ip_blocking_enabled, :boolean, default: false
+ remove_column :application_settings, :dnsbl_servers_list, :text
+ end
+end
diff --git a/db/migrate/20160128233227_change_lfs_objects_size_column.rb b/db/migrate/20160128233227_change_lfs_objects_size_column.rb
new file mode 100644
index 00000000000..e7fd1f71777
--- /dev/null
+++ b/db/migrate/20160128233227_change_lfs_objects_size_column.rb
@@ -0,0 +1,5 @@
+class ChangeLfsObjectsSizeColumn < ActiveRecord::Migration
+ def change
+ change_column :lfs_objects, :size, :integer, limit: 8
+ end
+end
diff --git a/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb
new file mode 100644
index 00000000000..d3ea956952e
--- /dev/null
+++ b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb
@@ -0,0 +1,80 @@
+class RemoveDotAtomPathEndingOfProjects < ActiveRecord::Migration
+ include Gitlab::ShellAdapter
+
+ class ProjectPath
+ attr_reader :old_path, :id, :namespace_path
+
+ def initialize(old_path, id, namespace_path, namespace_id)
+ @old_path = old_path
+ @id = id
+ @namespace_path = namespace_path
+ @namespace_id = namespace_id
+ end
+
+ def clean_path
+ @_clean_path ||= PathCleaner.clean(@old_path, @namespace_id)
+ end
+ end
+
+ class PathCleaner
+ def initialize(path, namespace_id)
+ @namespace_id = namespace_id
+ @path = path
+ end
+
+ def self.clean(*args)
+ new(*args).clean
+ end
+
+ def clean
+ path = cleaned_path
+ count = 0
+ while path_exists?(path)
+ path = "#{cleaned_path}#{count}"
+ count += 1
+ end
+ path
+ end
+
+ private
+
+ def cleaned_path
+ @_cleaned_path ||= @path.gsub(/\.atom\z/, '-atom')
+ end
+
+ def path_exists?(path)
+ Project.find_by_path_and_namespace_id(path, @namespace_id)
+ end
+ end
+
+ def projects_with_dot_atom
+ select_all("SELECT p.id, p.path, n.path as namespace_path, n.id as namespace_id FROM projects p inner join namespaces n on n.id = p.namespace_id WHERE p.path LIKE '%.atom'")
+ end
+
+ def up
+ projects_with_dot_atom.each do |project|
+ project_path = ProjectPath.new(project['path'], project['id'], project['namespace_path'], project['namespace_id'])
+ clean_path(project_path) if rename_project_repo(project_path)
+ end
+ end
+
+ private
+
+ def clean_path(project_path)
+ execute "UPDATE projects SET path = #{sanitize(project_path.clean_path)} WHERE id = #{project_path.id}"
+ end
+
+ def rename_project_repo(project_path)
+ old_path_with_namespace = File.join(project_path.namespace_path, project_path.old_path)
+ new_path_with_namespace = File.join(project_path.namespace_path, project_path.clean_path)
+
+ gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
+ gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
+ rescue
+ false
+ end
+
+ def sanitize(value)
+ ActiveRecord::Base.connection.quote(value)
+ end
+end
diff --git a/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb b/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb
new file mode 100644
index 00000000000..f0d94226514
--- /dev/null
+++ b/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb
@@ -0,0 +1,5 @@
+class AddMergeCommitShaToMergeRequests < ActiveRecord::Migration
+ def change
+ add_column :merge_requests, :merge_commit_sha, :string
+ end
+end
diff --git a/db/migrate/20160202091601_add_erasable_to_ci_build.rb b/db/migrate/20160202091601_add_erasable_to_ci_build.rb
new file mode 100644
index 00000000000..f9912f2274e
--- /dev/null
+++ b/db/migrate/20160202091601_add_erasable_to_ci_build.rb
@@ -0,0 +1,6 @@
+class AddErasableToCiBuild < ActiveRecord::Migration
+ def change
+ add_reference :ci_builds, :erased_by, references: :users, index: true
+ add_column :ci_builds, :erased_at, :datetime
+ end
+end
diff --git a/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb
new file mode 100644
index 00000000000..793984343b4
--- /dev/null
+++ b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb
@@ -0,0 +1,5 @@
+class AddAllowGuestToAccessBuildsProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :public_builds, :boolean, default: true, null: false
+ end
+end
diff --git a/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb b/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb
new file mode 100644
index 00000000000..f996ae74dca
--- /dev/null
+++ b/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb
@@ -0,0 +1,5 @@
+class AddRealSizeToMergeRequestDiffs < ActiveRecord::Migration
+ def change
+ add_column :merge_request_diffs, :real_size, :string
+ end
+end
diff --git a/db/migrate/20160209130428_add_index_to_snippet.rb b/db/migrate/20160209130428_add_index_to_snippet.rb
new file mode 100644
index 00000000000..95d5719be59
--- /dev/null
+++ b/db/migrate/20160209130428_add_index_to_snippet.rb
@@ -0,0 +1,5 @@
+class AddIndexToSnippet < ActiveRecord::Migration
+ def change
+ add_index :snippets, :updated_at
+ end
+end
diff --git a/db/migrate/20160212123307_create_tasks.rb b/db/migrate/20160212123307_create_tasks.rb
new file mode 100644
index 00000000000..c3f6f3abc26
--- /dev/null
+++ b/db/migrate/20160212123307_create_tasks.rb
@@ -0,0 +1,14 @@
+class CreateTasks < ActiveRecord::Migration
+ def change
+ create_table :tasks do |t|
+ t.references :user, null: false, index: true
+ t.references :project, null: false, index: true
+ t.references :target, polymorphic: true, null: false, index: true
+ t.integer :author_id, index: true
+ t.integer :action, null: false
+ t.string :state, null: false, index: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20160217100506_add_description_to_label.rb b/db/migrate/20160217100506_add_description_to_label.rb
new file mode 100644
index 00000000000..eed6d1f236a
--- /dev/null
+++ b/db/migrate/20160217100506_add_description_to_label.rb
@@ -0,0 +1,5 @@
+class AddDescriptionToLabel < ActiveRecord::Migration
+ def change
+ add_column :labels, :description, :string
+ end
+end
diff --git a/db/migrate/20160217174422_add_note_to_tasks.rb b/db/migrate/20160217174422_add_note_to_tasks.rb
new file mode 100644
index 00000000000..da5cb2e05db
--- /dev/null
+++ b/db/migrate/20160217174422_add_note_to_tasks.rb
@@ -0,0 +1,5 @@
+class AddNoteToTasks < ActiveRecord::Migration
+ def change
+ add_reference :tasks, :note, index: true
+ end
+end
diff --git a/db/migrate/20160220123949_rename_tasks_to_todos.rb b/db/migrate/20160220123949_rename_tasks_to_todos.rb
new file mode 100644
index 00000000000..30c10d27146
--- /dev/null
+++ b/db/migrate/20160220123949_rename_tasks_to_todos.rb
@@ -0,0 +1,5 @@
+class RenameTasksToTodos < ActiveRecord::Migration
+ def change
+ rename_table :tasks, :todos
+ end
+end
diff --git a/db/migrate/20160222153918_create_appearances_ce.rb b/db/migrate/20160222153918_create_appearances_ce.rb
new file mode 100644
index 00000000000..bec66bcc71e
--- /dev/null
+++ b/db/migrate/20160222153918_create_appearances_ce.rb
@@ -0,0 +1,14 @@
+class CreateAppearancesCe < ActiveRecord::Migration
+ def change
+ unless table_exists?(:appearances)
+ create_table :appearances do |t|
+ t.string :title
+ t.text :description
+ t.string :header_logo
+ t.string :logo
+
+ t.timestamps null: false
+ end
+ end
+ 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/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
new file mode 100644
index 00000000000..003169c13c6
--- /dev/null
+++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb
@@ -0,0 +1,53 @@
+class AddTrigramIndexesForSearching < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ unless trigrams_enabled?
+ raise 'You must enable the pg_trgm extension. You can do so by running ' \
+ '"CREATE EXTENSION pg_trgm;" as a PostgreSQL super user, this must be ' \
+ 'done for every GitLab database. For more information see ' \
+ 'http://www.postgresql.org/docs/current/static/sql-createextension.html'
+ end
+
+ # trigram indexes are case-insensitive so we can just index the column
+ # instead of indexing lower(column)
+ to_index.each do |table, columns|
+ columns.each do |column|
+ execute "CREATE INDEX CONCURRENTLY index_#{table}_on_#{column}_trigram ON #{table} USING gin(#{column} gin_trgm_ops);"
+ end
+ end
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ to_index.each do |table, columns|
+ columns.each do |column|
+ remove_index table, name: "index_#{table}_on_#{column}_trigram"
+ end
+ end
+ end
+
+ def trigrams_enabled?
+ res = execute("SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' AND installed_version IS NOT NULL;")
+ row = res.first
+
+ row && row['enabled'] == 't' ? true : false
+ end
+
+ def to_index
+ {
+ ci_runners: [:token, :description],
+ issues: [:title, :description],
+ merge_requests: [:title, :description],
+ milestones: [:title, :description],
+ namespaces: [:name, :path],
+ notes: [:note],
+ projects: [:name, :path, :description],
+ snippets: [:title, :file_name],
+ users: [:username, :name, :email]
+ }
+ end
+end
diff --git a/db/migrate/20160229193553_add_main_language_to_repository.rb b/db/migrate/20160229193553_add_main_language_to_repository.rb
new file mode 100644
index 00000000000..b5446c6a447
--- /dev/null
+++ b/db/migrate/20160229193553_add_main_language_to_repository.rb
@@ -0,0 +1,5 @@
+class AddMainLanguageToRepository < ActiveRecord::Migration
+ def change
+ add_column :projects, :main_language, :string
+ end
+end
diff --git a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb
new file mode 100644
index 00000000000..fc12b5b09e6
--- /dev/null
+++ b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb
@@ -0,0 +1,5 @@
+class RemoveExpiresAtFromSnippets < ActiveRecord::Migration
+ def change
+ remove_column :snippets, :expires_at, :datetime
+ end
+end
diff --git a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
new file mode 100644
index 00000000000..49e787d9a9a
--- /dev/null
+++ b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb
@@ -0,0 +1,9 @@
+class DisallowBlankLineCodeOnNote < ActiveRecord::Migration
+ def up
+ execute("UPDATE notes SET line_code = NULL WHERE line_code = ''")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/migrate/20160309140734_fix_todos.rb b/db/migrate/20160309140734_fix_todos.rb
new file mode 100644
index 00000000000..ebe0fc82305
--- /dev/null
+++ b/db/migrate/20160309140734_fix_todos.rb
@@ -0,0 +1,16 @@
+class FixTodos < ActiveRecord::Migration
+ def up
+ execute <<-SQL
+ DELETE FROM todos
+ WHERE todos.target_type IN ('Commit', 'ProjectSnippet')
+ OR NOT EXISTS (
+ SELECT *
+ FROM projects
+ WHERE projects.id = todos.project_id
+ )
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160310185910_add_external_flag_to_users.rb b/db/migrate/20160310185910_add_external_flag_to_users.rb
new file mode 100644
index 00000000000..54937f1eb71
--- /dev/null
+++ b/db/migrate/20160310185910_add_external_flag_to_users.rb
@@ -0,0 +1,5 @@
+class AddExternalFlagToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :external, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20160314143402_projects_add_pushes_since_gc.rb b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb
new file mode 100644
index 00000000000..5d30a38bc99
--- /dev/null
+++ b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb
@@ -0,0 +1,5 @@
+class ProjectsAddPushesSinceGc < ActiveRecord::Migration
+ def change
+ add_column :projects, :pushes_since_gc, :integer, default: 0
+ end
+end
diff --git a/db/migrate/20160316123110_ci_runners_token_index.rb b/db/migrate/20160316123110_ci_runners_token_index.rb
new file mode 100644
index 00000000000..67bf5b4f978
--- /dev/null
+++ b/db/migrate/20160316123110_ci_runners_token_index.rb
@@ -0,0 +1,13 @@
+class CiRunnersTokenIndex < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def change
+ args = [:ci_runners, :token]
+
+ if Gitlab::Database.postgresql?
+ args << { algorithm: :concurrently }
+ end
+
+ add_index(*args)
+ end
+end
diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index 2b7afae6d7c..14d7e84d856 100644
--- a/db/migrate/limits_to_mysql.rb
+++ b/db/migrate/limits_to_mysql.rb
@@ -6,5 +6,6 @@ class LimitsToMysql < ActiveRecord::Migration
change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647
+ change_column :events, :data, :text, limit: 2147483647
end
end
diff --git a/db/schema.rb b/db/schema.rb
index df7f72d5ad4..7e6863ef47e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,10 +11,11 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20151229112614) do
+ActiveRecord::Schema.define(version: 20160316204731) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ enable_extension "pg_trgm"
create_table "abuse_reports", force: :cascade do |t|
t.integer "reporter_id"
@@ -24,6 +25,15 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.datetime "updated_at"
end
+ create_table "appearances", force: :cascade do |t|
+ t.string "title"
+ t.text "description"
+ t.string "header_logo"
+ t.string "logo"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "application_settings", force: :cascade do |t|
t.integer "default_projects_limit"
t.boolean "signup_enabled"
@@ -54,8 +64,6 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.integer "two_factor_grace_period", default: 48
t.boolean "metrics_enabled", default: false
t.string "metrics_host", default: "localhost"
- t.string "metrics_username"
- t.string "metrics_password"
t.integer "metrics_pool_size", default: 16
t.integer "metrics_timeout", default: 10
t.integer "metrics_method_call_threshold", default: 10
@@ -63,6 +71,12 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.string "recaptcha_site_key"
t.string "recaptcha_private_key"
t.integer "metrics_port", default: 8089
+ t.integer "metrics_sample_interval", default: 15
+ t.boolean "sentry_enabled", default: false
+ t.string "sentry_dsn"
+ t.boolean "akismet_enabled", default: false
+ t.string "akismet_api_key"
+ t.boolean "email_author_in_body", default: false
end
create_table "audit_events", force: :cascade do |t|
@@ -83,7 +97,6 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
- t.integer "alert_type"
t.datetime "created_at"
t.datetime "updated_at"
t.string "color"
@@ -125,6 +138,9 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.string "description"
t.text "artifacts_file"
t.integer "gl_project_id"
+ t.text "artifacts_metadata"
+ t.integer "erased_by_id"
+ t.datetime "erased_at"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -132,6 +148,7 @@ ActiveRecord::Schema.define(version: 20151229112614) do
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
+ add_index "ci_builds", ["erased_by_id"], name: "index_ci_builds_on_erased_by_id", using: :btree
add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
@@ -242,6 +259,10 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.string "architecture"
end
+ add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
+ add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
+
create_table "ci_services", force: :cascade do |t|
t.string "type"
t.string "title"
@@ -395,17 +416,21 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
+ t.boolean "confidential", default: false
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", ["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
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
+ add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "keys", force: :cascade do |t|
t.integer "user_id"
@@ -438,14 +463,15 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "template", default: false
+ t.boolean "template", default: false
+ t.string "description"
end
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
- t.string "oid", null: false
- t.integer "size", null: false
+ t.string "oid", null: false
+ t.integer "size", limit: 8, null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "file"
@@ -491,6 +517,8 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.integer "merge_request_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.string "base_commit_sha"
+ t.string "real_size"
end
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
@@ -517,18 +545,21 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.text "merge_params"
t.boolean "merge_when_build_succeeds", default: false, null: false
t.integer "merge_user_id"
+ t.string "merge_commit_sha"
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", ["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
add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
+ add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "milestones", force: :cascade do |t|
t.string "title", null: false
@@ -542,27 +573,31 @@ ActiveRecord::Schema.define(version: 20151229112614) do
end
add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree
+ add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
+ add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
+ add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t|
- t.string "name", null: false
- t.string "path", null: false
+ t.string "name", null: false
+ t.string "path", null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
- t.string "description", default: "", null: false
+ t.string "description", default: "", null: false
t.string "avatar"
- t.boolean "public", default: false
+ t.boolean "share_with_group_lock", default: false
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
+ add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
- add_index "namespaces", ["public"], name: "index_namespaces_on_public", 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
create_table "notes", force: :cascade do |t|
@@ -588,6 +623,7 @@ ActiveRecord::Schema.define(version: 20151229112614) do
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
+ add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
@@ -637,6 +673,14 @@ ActiveRecord::Schema.define(version: 20151229112614) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "project_group_links", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "group_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "group_access", default: 30, null: false
+ end
+
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
@@ -676,6 +720,10 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.string "build_coverage_regex"
t.boolean "build_allow_git_fetch", default: true, null: false
t.integer "build_timeout", default: 3600, null: false
+ t.boolean "pending_delete", default: false
+ t.boolean "public_builds", default: true, null: false
+ t.string "main_language"
+ t.integer "pushes_since_gc", default: 0
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
@@ -683,9 +731,12 @@ ActiveRecord::Schema.define(version: 20151229112614) do
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
+ add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
+ add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
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", ["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
@@ -727,20 +778,24 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.string "type"
t.string "title"
t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "active", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "active", null: false
t.text "properties"
t.boolean "template", default: false
t.boolean "push_events", default: true
t.boolean "issues_events", default: true
t.boolean "merge_requests_events", default: true
t.boolean "tag_push_events", default: true
- t.boolean "note_events", default: true, null: false
- t.boolean "build_events", default: false, null: false
+ t.boolean "note_events", default: true, null: false
+ t.boolean "build_events", default: false, null: false
+ t.string "category", default: "common", null: false
+ t.boolean "default", default: false
end
+ add_index "services", ["category"], name: "index_services_on_category", using: :btree
add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
+ add_index "services", ["default"], name: "index_services_on_default", using: :btree
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
add_index "services", ["template"], name: "index_services_on_template", using: :btree
@@ -752,7 +807,6 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "file_name"
- t.datetime "expires_at"
t.string "type"
t.integer "visibility_level", default: 0, null: false
end
@@ -760,10 +814,25 @@ ActiveRecord::Schema.define(version: 20151229112614) do
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree
add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
- add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree
+ add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
+ add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
+ create_table "spam_logs", force: :cascade do |t|
+ t.integer "user_id"
+ t.string "source_ip"
+ t.string "user_agent"
+ t.boolean "via_api"
+ t.integer "project_id"
+ t.string "noteable_type"
+ t.string "title"
+ t.text "description"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "subscriptions", force: :cascade do |t|
t.integer "user_id"
t.integer "subscribable_id"
@@ -795,6 +864,26 @@ ActiveRecord::Schema.define(version: 20151229112614) do
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
+ 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.string "target_type", null: false
+ t.integer "author_id"
+ t.integer "action", null: false
+ t.string "state", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "note_id"
+ end
+
+ add_index "todos", ["author_id"], name: "index_todos_on_author_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
+ add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
+ add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -852,6 +941,8 @@ ActiveRecord::Schema.define(version: 20151229112614) do
t.boolean "hide_project_limit", default: false
t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
+ t.boolean "ldap_email", default: false, null: false
+ t.boolean "external", default: false
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -860,9 +951,12 @@ ActiveRecord::Schema.define(version: 20151229112614) do
add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
+ add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
add_index "users", ["name"], name: "index_users_on_name", using: :btree
+ add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
+ add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
create_table "users_star_projects", force: :cascade do |t|
t.integer "project_id", null: false
@@ -876,19 +970,19 @@ ActiveRecord::Schema.define(version: 20151229112614) do
add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
create_table "web_hooks", force: :cascade do |t|
- t.string "url"
+ t.string "url", limit: 2000
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type", default: "ProjectHook"
+ t.string "type", default: "ProjectHook"
t.integer "service_id"
- t.boolean "push_events", default: true, null: false
- t.boolean "issues_events", default: false, null: false
- t.boolean "merge_requests_events", default: false, null: false
- t.boolean "tag_push_events", default: false
- t.boolean "note_events", default: false, null: false
- t.boolean "enable_ssl_verification", default: true
- t.boolean "build_events", default: false, null: false
+ t.boolean "push_events", default: true, null: false
+ t.boolean "issues_events", default: false, null: false
+ t.boolean "merge_requests_events", default: false, null: false
+ t.boolean "tag_push_events", default: false
+ t.boolean "note_events", default: false, null: false
+ t.boolean "enable_ssl_verification", default: true
+ t.boolean "build_events", default: false, null: false
end
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
diff --git a/doc/README.md b/doc/README.md
index d9ef5051e3a..08d0a6a5bfb 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,62 +3,32 @@
## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API.
+- [CI](ci/README.md)
- [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).
- [Markdown](markdown/markdown.md) GitLab's advanced formatting system.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab
-- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do.
+- [Permissions](permissions/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
-- [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
+- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
-## CI Documentation
-
-- [Quick Start](ci/quick_start/README.md)
-- [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
-- [Configuring runner](ci/runners/README.md)
-- [Configuring deployment](ci/deployment/README.md)
-- [Using Docker Images](ci/docker/using_docker_images.md)
-- [Using Docker Build](ci/docker/using_docker_build.md)
-- [Using Variables](ci/variables/README.md)
-- [Using SSH keys](ci/ssh_keys/README.md)
-- [User permissions](ci/permissions/README.md)
-- [API](ci/api/README.md)
-- [Triggering builds through the API](ci/triggers/README.md)
-
-### CI Languages
-
-- [Testing PHP](ci/languages/php.md)
-
-### CI Services
-
-- [Using MySQL](ci/services/mysql.md)
-- [Using PostgreSQL](ci/services/postgres.md)
-- [Using Redis](ci/services/redis.md)
-- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
-
-### CI Examples
-
-- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
-- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
-- [Test Clojure applications](ci/examples/test-clojure-application.md)
-- Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
-
## Administrator documentation
-- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough.
+- [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.
- [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.
-- [Environmental Variables](administration/environment_variables.md) to configure GitLab.
+- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
-- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects.
+- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
@@ -66,9 +36,13 @@
- [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
+- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
+- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics
## Contributor documentation
+- [Documentation styleguide](development/doc_styleguide.md) Use this styleguide if you are
+ 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/enviroment_variables.md b/doc/administration/enviroment_variables.md
deleted file mode 100644
index 7d8f9d29eef..00000000000
--- a/doc/administration/enviroment_variables.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Environment Variables
-
-## Introduction
-
-Commonly people configure GitLab via the gitlab.rb configuration file in the Omnibus package.
-
-But if you prefer to use environment variables we allow that too.
-
-## Supported environment variables
-
-Variable | Type | Explanation
--------- | ---- | -----------
-GITLAB_ROOT_PASSWORD | string | sets the password for the `root` user on installation
-GITLAB_HOST | url | hostname of the GitLab server includes http or https
-RAILS_ENV | production / development / staging / test | Rails environment
-DATABASE_URL | url | For example: postgresql://localhost/blog_development?pool=5
-GITLAB_EMAIL_FROM | email | Email address used in the "From" field in mails sent by GitLab
-GITLAB_EMAIL_DISPLAY_NAME | string | Name used in the "From" field in mails sent by GitLab
-GITLAB_EMAIL_REPLY_TO | email | Email address used in the "Reply-To" field in mails sent by GitLab
-
-## Complete database variables
-
-As explained in the [Heroku documentation](https://devcenter.heroku.com/articles/rails-database-connection-behavior) the DATABASE_URL doesn't let you set:
-
-- adapter
-- database
-- username
-- password
-- host
-- port
-
-To do so please `cp config/database.yml.env config/database.yml` and use the following variables:
-
-Variable | Default
---- | ---
-GITLAB_DATABASE_ADAPTER | postgresql
-GITLAB_DATABASE_ENCODING | unicode
-GITLAB_DATABASE_DATABASE | gitlab_#{ENV['RAILS_ENV']
-GITLAB_DATABASE_POOL | 10
-GITLAB_DATABASE_USERNAME | root
-GITLAB_DATABASE_PASSWORD |
-GITLAB_DATABASE_HOST | localhost
-GITLAB_DATABASE_PORT | 5432
-
-## Other variables
-
-We welcome merge requests to make more settings configurable via variables.
-Please stick to the naming scheme "GITLAB_#{name 1_settings.rb in upper case}".
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
new file mode 100644
index 00000000000..43ab153d76d
--- /dev/null
+++ b/doc/administration/environment_variables.md
@@ -0,0 +1,61 @@
+# Environment Variables
+
+GitLab exposes certain environment variables which can be used to override
+their defaults values.
+
+People usually configure GitLab via `/etc/gitlab/gitlab.rb` for Omnibus
+installations, or `gitlab.yml` for installations from source.
+
+Below you will find the supported environment variables which you can use to
+override certain values.
+
+## Supported environment variables
+
+Variable | Type | Description
+-------- | ---- | -----------
+`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
+`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
+`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
+`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
+`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+
+## Complete database variables
+
+The recommended way of specifying your database connection information is to set
+the `DATABASE_URL` environment variable. This variable only holds connection
+information (`adapter`, `database`, `username`, `password`, `host` and `port`),
+but not behavior information (`encoding`, `pool`). If you don't want to use
+`DATABASE_URL` and/or want to set database behavior information, you will have
+to either:
+
+- copy our template file: `cp config/database.yml.env config/database.yml`, or
+- set a value for some `GITLAB_DATABASE_XXX` variables
+
+The list of `GITLAB_DATABASE_XXX` variables that you can set is:
+
+Variable | Default value | Overridden by `DATABASE_URL`?
+-------- | ------------- | -----------------------------
+`GITLAB_DATABASE_ADAPTER` | `postgresql` (for MySQL use `mysql2`) | Yes
+`GITLAB_DATABASE_DATABASE` | `gitlab_#{ENV['RAILS_ENV']` | Yes
+`GITLAB_DATABASE_USERNAME` | `root` | Yes
+`GITLAB_DATABASE_PASSWORD` | None | Yes
+`GITLAB_DATABASE_HOST` | `localhost` | Yes
+`GITLAB_DATABASE_PORT` | `5432` | Yes
+`GITLAB_DATABASE_ENCODING` | `unicode` | No
+`GITLAB_DATABASE_POOL` | `10` | No
+
+## Adding more variables
+
+We welcome merge requests to make more settings configurable via variables.
+Please make changes in the `config/initializers/1_settings.rb` file and stick
+to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`.
+
+## Omnibus configuration
+
+It's possible to preconfigure the GitLab docker image by adding the environment
+variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command.
+For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://doc.gitlab.com/omnibus/docker/#preconfigure-docker-container).
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
new file mode 100644
index 00000000000..a5fa7d358a2
--- /dev/null
+++ b/doc/administration/housekeeping.md
@@ -0,0 +1,22 @@
+# Housekeeping
+
+_**Note:** This feature was [introduced][ce-2371] in GitLab 8.4_
+
+---
+
+The housekeeping function runs `git gc` ([man page][man]) on the current
+project Git repository.
+
+`git gc` runs a number of housekeeping tasks, such as compressing file
+revisions (to reduce disk space and increase performance) and removing
+unreachable objects which may have been created from prior invocations of
+`git add`.
+
+You can find this option under your **[Project] > Settings**.
+
+---
+
+![Housekeeping settings](img/housekeeping_settings.png)
+
+[ce-2371]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2371 "Housekeeping merge request"
+[man]: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html "git gc man page"
diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png
new file mode 100644
index 00000000000..f7c5bc44367
--- /dev/null
+++ b/doc/administration/img/housekeeping_settings.png
Binary files differ
diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md
new file mode 100644
index 00000000000..483060395dd
--- /dev/null
+++ b/doc/administration/restart_gitlab.md
@@ -0,0 +1,145 @@
+# How to restart GitLab
+
+Depending on how you installed GitLab, there are different methods to restart
+its service(s).
+
+If you want the TL;DR versions, jump to:
+
+- [Omnibus GitLab restart](#omnibus-gitlab-restart)
+- [Omnibus GitLab reconfigure](#omnibus-gitlab-reconfigure)
+- [Source installation restart](#installations-from-source)
+
+## Omnibus installations
+
+If you have used the [Omnibus packages][omnibus-dl] to install GitLab, then
+you should already have `gitlab-ctl` in your `PATH`.
+
+`gitlab-ctl` interacts with the Omnibus packages and can be used to restart the
+GitLab Rails application (Unicorn) as well as the other components, like:
+
+- GitLab Workhorse
+- Sidekiq
+- PostgreSQL (if you are using the bundled one)
+- NGINX (if you are using the bundled one)
+- Redis (if you are using the bundled one)
+- [Mailroom][]
+- Logrotate
+
+### Omnibus GitLab restart
+
+There may be times in the documentation where you will be asked to _restart_
+GitLab. In that case, you need to run the following command:
+
+```bash
+sudo gitlab-ctl restart
+```
+
+The output should be similar to this:
+
+```
+ok: run: gitlab-workhorse: (pid 11291) 1s
+ok: run: logrotate: (pid 11299) 0s
+ok: run: mailroom: (pid 11306) 0s
+ok: run: nginx: (pid 11309) 0s
+ok: run: postgresql: (pid 11316) 1s
+ok: run: redis: (pid 11325) 0s
+ok: run: sidekiq: (pid 11331) 1s
+ok: run: unicorn: (pid 11338) 0s
+```
+
+To restart a component separately, you can append its service name to the
+`restart` command. For example, to restart **only** NGINX you would run:
+
+```bash
+sudo gitlab-ctl restart nginx
+```
+
+To check the status of GitLab services, run:
+
+```bash
+sudo gitlab-ctl status
+```
+
+Notice that all services say `ok: run`.
+
+Sometimes, components time out during the restart and sometimes they get stuck.
+In that case, you can use `gitlab-ctl kill <service>` to send the `SIGKILL`
+signal to the service, for example `sidekiq`. After that, a restart should
+perform fine.
+
+As a last resort, you can try to
+[reconfigure GitLab](#omnibus-gitlab-reconfigure) instead.
+
+### Omnibus GitLab reconfigure
+
+There may be times in the documentation where you will be asked to _reconfigure_
+GitLab. Remember that this method applies only for the Omnibus packages.
+
+Reconfigure Omnibus GitLab with:
+
+```bash
+sudo gitlab-ctl reconfigure
+```
+
+Reconfiguring GitLab should occur in the event that something in its
+configuration (`/etc/gitlab/gitlab.rb`) has changed.
+
+When you run this command, [Chef], the underlying configuration management
+application that powers Omnibus GitLab, will make sure that all directories,
+permissions, services, etc., are in place and in the same shape that they were
+initially shipped.
+
+It will also restart GitLab components where needed, if any of their
+configuration files have changed.
+
+If you manually edit any files in `/var/opt/gitlab` that are managed by Chef,
+running reconfigure will revert the changes AND restart the services that
+depend on those files.
+
+## Installations from source
+
+If you have followed the official installation guide to [install GitLab from
+source][install], run the following command to restart GitLab:
+
+```
+sudo service gitlab restart
+```
+
+The output should be similar to this:
+
+```
+Shutting down GitLab Unicorn
+Shutting down GitLab Sidekiq
+Shutting down GitLab Workhorse
+Shutting down GitLab MailRoom
+...
+GitLab is not running.
+Starting GitLab Unicorn
+Starting GitLab Sidekiq
+Starting GitLab Workhorse
+Starting GitLab MailRoom
+...
+The GitLab Unicorn web server with pid 28059 is running.
+The GitLab Sidekiq job dispatcher with pid 28176 is running.
+The GitLab Workhorse with pid 28122 is running.
+The GitLab MailRoom email processor with pid 28114 is running.
+GitLab and all its components are up and running.
+```
+
+This should restart Unicorn, Sidekiq, GitLab Workhorse and [Mailroom][]
+(if enabled). The init service file that does all the magic can be found on
+your server in `/etc/init.d/gitlab`.
+
+---
+
+If you are using other init systems, like systemd, you can check the
+[GitLab Recipes][gl-recipes] repository for some unofficial services. These are
+**not** officially supported so use them at your own risk.
+
+
+[omnibus-dl]: https://about.gitlab.com/downloads/ "Download the Omnibus packages"
+[install]: ../install/installation.md "Documentation to install GitLab from source"
+[mailroom]: ../incoming_email/README.md "Used for replying by email in GitLab issues and merge requests"
+[chef]: https://www.chef.io/chef/ "Chef official website"
+[src-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab "GitLab init service file"
+[gl-recipes]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/init "GitLab Recipes repository"
diff --git a/doc/api/README.md b/doc/api/README.md
index 25a31b235cc..7629ef294ac 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -1,7 +1,13 @@
# GitLab API
+Automate GitLab via a simple and powerful API. All definitions can be found
+under [`/lib/api`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api).
+
## Resources
+Documentation for various API resources can be found separately in the
+following locations:
+
- [Users](users.md)
- [Session](session.md)
- [Projects](projects.md) including setting Webhooks
@@ -23,17 +29,20 @@
- [Namespaces](namespaces.md)
- [Settings](settings.md)
- [Keys](keys.md)
+- [Builds](builds.md)
+- [Build triggers](build_triggers.md)
+- [Build Variables](build_variables.md)
+- [Runners](runners.md)
-## Clients
-
-Find API Clients for GitLab [on our website](https://about.gitlab.com/applications/#api-clients).
-You can use [GitLab as an OAuth2 client](oauth2.md) to make API calls.
+## Authentication
-## Introduction
+All API requests require authentication. You need to pass a `private_token`
+parameter via query string or header. If passed as a header, the header name
+must be `PRIVATE-TOKEN` (uppercase and with a dash instead of an underscore).
+You can find or reset your private token in your account page (`/profile/account`).
-All API requests require authentication. You need to pass a `private_token` parameter by URL or header. If passed as header, the header name must be "PRIVATE-TOKEN" (capital and with dash instead of underscore). You can find or reset your private token in your profile.
-
-If no, or an invalid, `private_token` is provided then an error message will be returned with status code 401:
+If `private_token` is invalid or omitted, then an error message will be
+returned with status code `401`:
```json
{
@@ -41,71 +50,83 @@ If no, or an invalid, `private_token` is provided then an error message will be
}
```
-API requests should be prefixed with `api` and the API version. The API version is defined in `lib/api.rb`.
+API requests should be prefixed with `api` and the API version. The API version
+is defined in [`lib/api.rb`][lib-api-url].
Example of a valid API request:
-```
-GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U
+```shell
+GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
```
-Example for a valid API request using curl and authentication via header:
+Example of a valid API request using cURL and authentication via header:
-```
-curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://example.com/api/v3/projects"
+```shell
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
```
-The API uses JSON to serialize data. You don't need to specify `.json` at the end of API URL.
+The API uses JSON to serialize data. You don't need to specify `.json` at the
+end of an API URL.
## Authentication with OAuth2 token
-Instead of the private_token you can transmit the OAuth2 access token as a header or as a parameter.
+Instead of the `private_token` you can transmit the OAuth2 access token as a
+header or as a parameter.
-### OAuth2 token (as a parameter)
+Example of OAuth2 token as a parameter:
-```
-curl https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN
+```shell
+curl https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN
```
-### OAuth2 token (as a header)
+Example of OAuth2 token as a header:
-```
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user
+```shell
+curl -H "Authorization: Bearer OAUTH-TOKEN" https://example.com/api/v3/user
```
Read more about [GitLab as an OAuth2 client](oauth2.md).
## Status codes
-The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave.
-
-API request types:
-
-- `GET` requests access one or more resources and return the result as JSON
-- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON
-- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON
-- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not.
-
-The following list shows the possible return codes for API requests.
-
-Return values:
-
-- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON
-- `201 Created` - The `POST` request was successful and the resource is returned as JSON
-- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given
-- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above
-- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project
-- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found
-- `405 Method Not Allowed` - The request is not supported
-- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists
-- `422 Unprocessable` - The entity could not be processed
-- `500 Server Error` - While handling the request something went wrong on the server side
+The API is designed to return different status codes according to context and
+action. This way, if a request results in an error, the caller is able to get
+insight into what went wrong.
+
+The following table gives an overview of how the API functions generally behave.
+
+| Request type | Description |
+| ------------ | ----------- |
+| `GET` | Access one or more resources and return the result as JSON. |
+| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
+| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. |
+| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. |
+
+The following table shows the possible return codes for API requests.
+
+| Return values | Description |
+| ------------- | ----------- |
+| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
+| `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
+| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
+| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. |
+| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. |
+| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. |
+| `405 Method Not Allowed` | The request is not supported. |
+| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
+| `422 Unprocessable` | The entity could not be processed. |
+| `500 Server Error` | While handling the request something went wrong server-side. |
## Sudo
-All API requests support performing an api call as if you were another user, if your private token is for an administration account. You need to pass `sudo` parameter by URL or header with an id or username of the user you want to perform the operation as. If passed as header, the header name must be "SUDO" (capitals).
+All API requests support performing an API call as if you were another user,
+provided your private token is from an administrator account. You need to pass
+the `sudo` parameter either via query string or a header with an ID/username of
+the user you want to perform the operation as. If passed as a header, the
+header name must be `SUDO` (uppercase).
-If a non administrative `private_token` is provided then an error message will be returned with status code 403:
+If a non administrative `private_token` is provided, then an error message will
+be returned with status code `403`:
```json
{
@@ -113,7 +134,8 @@ If a non administrative `private_token` is provided then an error message will b
}
```
-If the sudo user id or username cannot be found then an error message will be returned with status code 404:
+If the sudo user ID or username cannot be found, an error message will be
+returned with status code `404`:
```json
{
@@ -121,94 +143,181 @@ If the sudo user id or username cannot be found then an error message will be re
}
```
-Example of a valid API with sudo request:
+---
+
+Example of a valid API call and a request using cURL with sudo request,
+providing a username:
+```shell
+GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username
```
-GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=username
+
+```shell
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v3/projects"
```
+Example of a valid API call and a request using cURL with sudo request,
+providing an ID:
+
+```shell
+GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
```
-GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=23
+
+```shell
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v3/projects"
```
-Example for a valid API request with sudo using curl and authentication via header:
+## Pagination
+
+Sometimes the returned result will span across many pages. When listing
+resources you can pass the following parameters:
+| Parameter | Description |
+| --------- | ----------- |
+| `page` | Page number (default: `1`) |
+| `per_page`| Number of items to list per page (default: `20`, max: `100`) |
+
+In the example below, we list 50 [namespaces](namespaces.md) per page.
+
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50
```
-curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: username" "http://example.com/api/v3/projects"
+
+### Pagination Link header
+
+[Link headers](http://www.w3.org/wiki/LinkHeader) are sent back with each
+response. They have `rel` set to prev/next/first/last and contain the relevant
+URL. Please use these links instead of generating your own URLs.
+
+In the cURL example below, we limit the output to 3 items per page (`per_page=3`)
+and we request the second page (`page=2`) of [comments](notes.md) of the issue
+with ID `8` which belongs to the project with ID `8`:
+
+```bash
+curl -I -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2
```
+The response will then be:
+
```
-curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: 23" "http://example.com/api/v3/projects"
+HTTP/1.1 200 OK
+Cache-Control: no-cache
+Content-Length: 1103
+Content-Type: application/json
+Date: Mon, 18 Jan 2016 09:43:18 GMT
+Link: <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=3&per_page=3>; rel="last"
+Status: 200 OK
+Vary: Origin
+X-Next-Page: 3
+X-Page: 2
+X-Per-Page: 3
+X-Prev-Page: 1
+X-Request-Id: 732ad4ee-9870-4866-a199-a9db0cde3c86
+X-Runtime: 0.108688
+X-Total: 8
+X-Total-Pages: 3
```
-## Pagination
+### Other pagination headers
-When listing resources you can pass the following parameters:
+Additional pagination headers are also sent back.
-- `page` (default: `1`) - page number
-- `per_page` (default: `20`, max: `100`) - number of items to list per page
+| Header | Description |
+| ------ | ----------- |
+| `X-Total` | The total number of items |
+| `X-Total-Pages` | The total number of pages |
+| `X-Per-Page` | The number of items per page |
+| `X-Page` | The index of the current page (starting at 1) |
+| `X-Next-Page` | The index of the next page |
+| `X-Prev-Page` | The index of the previous page |
-[Link headers](http://www.w3.org/wiki/LinkHeader) are send back with each response. These have `rel` prev/next/first/last and contain the relevant URL. Please use these instead of generating your own URLs.
+## `id` vs `iid`
-## id vs iid
+When you work with the API, you may notice two similar fields in API entities:
+`id` and `iid`. The main difference between them is scope.
-When you work with API you may notice two similar fields in api entities: id and iid. The main difference between them is scope. Example:
+For example, an issue might have `id: 46` and `iid: 5`.
-Issue:
+| Parameter | Description |
+| --------- | ----------- |
+| `id` | Is unique across all issues and is used for any API call |
+| `iid` | Is unique only in scope of a single project. When you browse issues or merge requests with the Web UI, you see the `iid` |
- id: 46
- iid: 5
+That means that if you want to get an issue via the API you should use the `id`:
-- id - is unique across all issues. It's used for any api call.
-- iid - is unique only in scope of a single project. When you browse issues or merge requests with Web UI, you see iid.
+```bash
+GET /projects/42/issues/:id
+```
-So if you want to get issue with api you use `http://host/api/v3/.../issues/:id.json`. But when you want to create a link to web page - use `http:://host/project/issues/:iid.json`
+On the other hand, if you want to create a link to a web page you should use
+the `iid`:
+
+```bash
+GET /projects/42/issues/:iid
+```
## Data validation and error reporting
-When working with the API you may encounter validation errors. In such case, the API will answer with an HTTP `400` status.
+When working with the API you may encounter validation errors, in which case
+the API will answer with an HTTP `400` status.
+
Such errors appear in two cases:
-* A required attribute of the API request is missing, e.g. the title of an issue is not given
-* An attribute did not pass the validation, e.g. user bio is too long
+- A required attribute of the API request is missing, e.g., the title of an
+ issue is not given
+- An attribute did not pass the validation, e.g., user bio is too long
When an attribute is missing, you will get something like:
- HTTP/1.1 400 Bad Request
- Content-Type: application/json
-
- {
- "message":"400 (Bad request) \"title\" not given"
- }
-
-When a validation error occurs, error messages will be different. They will hold all details of validation errors:
+```
+HTTP/1.1 400 Bad Request
+Content-Type: application/json
+{
+ "message":"400 (Bad request) \"title\" not given"
+}
+```
- HTTP/1.1 400 Bad Request
- Content-Type: application/json
+When a validation error occurs, error messages will be different. They will
+hold all details of validation errors:
- {
- "message": {
- "bio": [
- "is too long (maximum is 255 characters)"
- ]
- }
+```
+HTTP/1.1 400 Bad Request
+Content-Type: application/json
+{
+ "message": {
+ "bio": [
+ "is too long (maximum is 255 characters)"
+ ]
}
+}
+```
-This makes error messages more machine-readable. The format can be described as follow:
+This makes error messages more machine-readable. The format can be described as
+follows:
- {
- "message": {
+```json
+{
+ "message": {
+ "<property-name>": [
+ "<error-message>",
+ "<error-message>",
+ ...
+ ],
+ "<embed-entity>": {
"<property-name>": [
"<error-message>",
"<error-message>",
...
],
- "<embed-entity>": {
- "<property-name>": [
- "<error-message>",
- "<error-message>",
- ...
- ],
- }
}
}
+}
+```
+
+## Clients
+
+There are many unofficial GitLab API Clients for most of the popular
+programming languages. Visit the [GitLab website] for a complete list.
+
+[GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API"
+[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 6a9c10c8520..abc4732c395 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -8,13 +8,21 @@ Get a list of repository branches from a project, sorted by name alphabetically.
GET /projects/:id/repository/branches
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
-- `id` (required) - The ID of a project
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches
+```
+
+Example response:
```json
[
{
+ "name": "master",
+ "protected": true,
"commit": {
"author_email": "john@example.com",
"author_name": "John Smith",
@@ -27,10 +35,9 @@ Parameters:
"parent_ids": [
"4ad91d3c1144c406e50c7b33bae684bd6837faf8"
]
- },
- "name": "master",
- "protected": true
- }
+ }
+ },
+ ...
]
```
@@ -42,13 +49,21 @@ Get a single project repository branch.
GET /projects/:id/repository/branches/:branch
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `branch` | string | yes | The name of the branch |
-- `id` (required) - The ID of a project
-- `branch` (required) - The name of the branch
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master
+```
+
+Example response:
```json
{
+ "name": "master",
+ "protected": true,
"commit": {
"author_email": "john@example.com",
"author_name": "John Smith",
@@ -61,25 +76,30 @@ Parameters:
"parent_ids": [
"4ad91d3c1144c406e50c7b33bae684bd6837faf8"
]
- },
- "name": "master",
- "protected": true
+ }
}
```
## Protect repository branch
-Protects a single project repository branch. This is an idempotent function, protecting an already
-protected repository branch still returns a `200 OK` status code.
+Protects a single project repository branch. This is an idempotent function,
+protecting an already protected repository branch still returns a `200 OK`
+status code.
```
PUT /projects/:id/repository/branches/:branch/protect
```
-Parameters:
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect
+```
-- `id` (required) - The ID of a project
-- `branch` (required) - The name of the branch
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `branch` | string | yes | The name of the branch |
+
+Example response:
```json
{
@@ -103,17 +123,24 @@ Parameters:
## Unprotect repository branch
-Unprotects a single project repository branch. This is an idempotent function, unprotecting an already
-unprotected repository branch still returns a `200 OK` status code.
+Unprotects a single project repository branch. This is an idempotent function,
+unprotecting an already unprotected repository branch still returns a `200 OK`
+status code.
```
PUT /projects/:id/repository/branches/:branch/unprotect
```
-Parameters:
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `branch` | string | yes | The name of the branch |
-- `id` (required) - The ID of a project
-- `branch` (required) - The name of the branch
+Example response:
```json
{
@@ -141,11 +168,17 @@ Parameters:
POST /projects/:id/repository/branches
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `branch_name` | string | yes | The name of the branch |
+| `ref` | string | yes | The branch name or commit SHA to create branch from |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master"
+```
-- `id` (required) - The ID of a project
-- `branch_name` (required) - The name of the branch
-- `ref` (required) - Create branch from commit SHA or existing branch
+Example response:
```json
{
@@ -162,12 +195,13 @@ Parameters:
"4ad91d3c1144c406e50c7b33bae684bd6837faf8"
]
},
- "name": "master",
+ "name": "newbranch",
"protected": false
}
```
-It return 200 if succeed or 400 if failed with error message explaining reason.
+It returns `200` if it succeeds or `400` if failed with an error message
+explaining the reason.
## Delete repository branch
@@ -175,18 +209,22 @@ It return 200 if succeed or 400 if failed with error message explaining reason.
DELETE /projects/:id/repository/branches/:branch
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `branch` | string | yes | The name of the branch |
-- `id` (required) - The ID of a project
-- `branch` (required) - The name of the branch
+It returns `200` if it succeeds, `404` if the branch to be deleted does not exist
+or `400` for other reasons. In case of an error, an explaining message is provided.
-It return 200 if succeed, 404 if the branch to be deleted does not exist
-or 400 for other reasons. In case of an error, an explaining message is provided.
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
+```
-Success response:
+Example response:
```json
{
- "branch_name": "my-removed-branch"
+ "branch_name": "newbranch"
}
```
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
new file mode 100644
index 00000000000..4a12e962b62
--- /dev/null
+++ b/doc/api/build_triggers.md
@@ -0,0 +1,108 @@
+# Build triggers
+
+You can read more about [triggering builds through the API](../ci/triggers/README.md).
+
+## List project triggers
+
+Get a list of project's build triggers.
+
+```
+GET /projects/:id/triggers
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+```
+
+```json
+[
+ {
+ "created_at": "2015-12-23T16:24:34.716Z",
+ "deleted_at": null,
+ "last_used": "2016-01-04T15:41:21.986Z",
+ "token": "fbdb730c2fbdb095a0862dbd8ab88b",
+ "updated_at": "2015-12-23T16:24:34.716Z"
+ },
+ {
+ "created_at": "2015-12-23T16:25:56.760Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "7b9148c158980bbd9bcea92c17522d",
+ "updated_at": "2015-12-23T16:25:56.760Z"
+ }
+]
+```
+
+## Get trigger details
+
+Get details of project's build trigger.
+
+```
+GET /projects/:id/triggers/:token
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | The `token` of a trigger |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+```
+
+```json
+{
+ "created_at": "2015-12-23T16:25:56.760Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "7b9148c158980bbd9bcea92c17522d",
+ "updated_at": "2015-12-23T16:25:56.760Z"
+}
+```
+
+## Create a project trigger
+
+Create a build trigger for a project.
+
+```
+POST /projects/:id/triggers
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+```
+
+```json
+{
+ "created_at": "2016-01-07T09:53:58.235Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+ "updated_at": "2016-01-07T09:53:58.235Z"
+}
+```
+
+## Remove a project trigger
+
+Remove a project's build trigger.
+
+```
+DELETE /projects/:id/triggers/:token
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | The `token` of a project |
+
+```
+curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+```
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
new file mode 100644
index 00000000000..b96f1bdac8a
--- /dev/null
+++ b/doc/api/build_variables.md
@@ -0,0 +1,128 @@
+# Build Variables
+
+## List project variables
+
+Get list of a project's build variables.
+
+```
+GET /projects/:id/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+```
+
+```json
+[
+ {
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+ },
+ {
+ "key": "TEST_VARIABLE_2",
+ "value": "TEST_2"
+ }
+]
+```
+
+## Show variable details
+
+Get the details of a project's specific build variable.
+
+```
+GET /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+```
+
+```json
+{
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+}
+```
+
+## Create variable
+
+Create a new build variable.
+
+```
+POST /projects/:id/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "new value"
+}
+```
+
+## Update variable
+
+Update a project's build variable.
+
+```
+PUT /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+
+```
+curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "updated value"
+}
+```
+
+## Remove variable
+
+Remove a project's build variable.
+
+```
+DELETE /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+
+```
+curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
+```
+
+```json
+{
+ "key": "VARIABLE_1",
+ "value": "VALUE_1"
+}
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
new file mode 100644
index 00000000000..4c0a47d1ea0
--- /dev/null
+++ b/doc/api/builds.md
@@ -0,0 +1,421 @@
+# Builds API
+
+## List project builds
+
+Get a list of builds in a project.
+
+```
+GET /projects/:id/builds
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+
+```
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds"
+```
+
+Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
+ },
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "id": 7,
+ "name": "teaspoon",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "id": 6,
+ "name": "spinach:other",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:24.729Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## List commit builds
+
+Get a list of builds for specific commit in a project.
+
+```
+GET /projects/:id/repository/commits/:sha/builds
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `sha` | string | yes | The SHA id of a commit |
+| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+
+```
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds"
+```
+
+Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.957Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:33.913Z",
+ "id": 9,
+ "name": "brakeman",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:33.727Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## Get a single build
+
+Get a single build of a project
+
+```
+GET /projects/:id/builds/:build_id
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+```
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.880Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:31.198Z",
+ "id": 8,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:30.733Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+}
+```
+
+## Get build artifacts
+
+> [Introduced][ce-2893] in GitLab 8.5
+
+Get build artifacts of a project
+
+```
+GET /projects/:id/builds/:build_id/artifacts
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+```
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 200 | Serves the artifacts file |
+| 404 | Build not found or no artifacts |
+
+[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
+
+## Cancel a build
+
+Cancel a single build of a project
+
+```
+POST /projects/:id/builds/:build_id/cancel
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+```
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
+}
+```
+
+## Retry a build
+
+Retry a single build of a project
+
+```
+POST /projects/:id/builds/:build_id/retry
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+```
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "pending",
+ "tag": false,
+ "user": null
+}
+```
+
+## Erase a build
+
+Erase a single build of a project (remove build artifacts and a build trace)
+
+```
+POST /projects/:id/builds/:build_id/erase
+```
+
+Parameters
+
+| Attribute | Type | required | Description |
+|-------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+Example of request
+
+```
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "download_url": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "started_at": "2016-01-11T10:13:33.506Z",
+ "finished_at": "2016-01-11T10:15:10.506Z",
+ "status": "failed",
+ "tag": false,
+ "user": null
+}
+```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 93d62b751e6..6341440c58b 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -8,10 +8,16 @@ Get a list of repository commits in a project.
GET /projects/:id/repository/commits
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits"
+```
-- `id` (required) - The ID of a project
-- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch
+Example response:
```json
[
@@ -48,8 +54,16 @@ GET /projects/:id/repository/commits/:sha
Parameters:
-- `id` (required) - The ID of a project
-- `sha` (required) - The commit hash or name of a repository branch or tag
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `sha` | string | yes | The commit hash or name of a repository branch or tag |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master
+```
+
+Example response:
```json
{
@@ -79,8 +93,16 @@ GET /projects/:id/repository/commits/:sha/diff
Parameters:
-- `id` (required) - The ID of a project
-- `sha` (required) - The name of a repository branch or tag or if not given the default branch
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `sha` | string | yes | The commit hash or name of a repository branch or tag |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff"
+```
+
+Example response:
```json
[
@@ -107,8 +129,16 @@ GET /projects/:id/repository/commits/:sha/comments
Parameters:
-- `id` (required) - The ID of a project
-- `sha` (required) - The name of a repository branch or tag or if not given the default branch
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `sha` | string | yes | The commit hash or name of a repository branch or tag |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments"
+```
+
+Example response:
```json
[
@@ -128,39 +158,64 @@ Parameters:
## Post comment to commit
-Adds a comment to a commit. Optionally you can post comments on a specific line of a commit. Therefor both `path`, `line_new` and `line_old` are required.
+Adds a comment to a commit.
+
+In order to post a comment in a particular line of a particular file, you must
+specify the full commit SHA, the `path`, the `line` and `line_type` should be
+`new`.
+
+The comment will be added at the end of the last commit if at least one of the
+cases below is valid:
+
+- the `sha` is instead a branch or a tag and the `line` or `path` are invalid
+- the `line` number is invalid (does not exist)
+- the `path` is invalid (does not exist)
+
+In any of the above cases, the response of `line`, `line_type` and `path` is
+set to `null`.
```
POST /projects/:id/repository/commits/:sha/comments
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `sha` | string | yes | The commit SHA or name of a repository branch or tag |
+| `note` | string | yes | The text of the comment |
+| `path` | string | no | The file path relative to the repository |
+| `line` | integer | no | The line number where the comment should be placed |
+| `line_type` | string | no | The line type. Takes `new` or `old` as arguments |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "note=Nice picture man\!" -F "path=dudeism.md" -F "line=11" -F "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
+```
-- `id` (required) - The ID of a project
-- `sha` (required) - The name of a repository branch or tag or if not given the default branch
-- `note` (required) - Text of comment
-- `path` (optional) - The file path
-- `line` (optional) - The line number
-- `line_type` (optional) - The line type (new or old)
+Example response:
```json
{
- "author": {
- "id": 1,
- "username": "admin",
- "email": "admin@local.host",
- "name": "Administrator",
- "blocked": false,
- "created_at": "2012-04-29T08:46:00Z"
- },
- "note": "text1",
- "path": "example.rb",
- "line": 5,
- "line_type": "new"
+ "author" : {
+ "web_url" : "https://gitlab.example.com/u/thedude",
+ "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
+ "username" : "thedude",
+ "state" : "active",
+ "name" : "Jeff Lebowski",
+ "id" : 28
+ },
+ "created_at" : "2016-01-19T09:44:55.600Z",
+ "line_type" : "new",
+ "path" : "dudeism.md",
+ "line" : 11,
+ "note" : "Nice picture man!"
}
```
-## Get the status of a commit
+## Commit status
+
+Since GitLab 8.1, this is the new commit status API.
+
+### Get the status of a commit
Get the statuses of a commit in a project.
@@ -168,75 +223,116 @@ Get the statuses of a commit in a project.
GET /projects/:id/repository/commits/:sha/statuses
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project
+| `sha` | string | yes | The commit SHA
+| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch
+| `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test`
+| `name` | string | no | Filter by [job name](../ci/yaml/README.md#jobs), e.g., `bundler:audit`
+| `all` | boolean | no | Return all statuses, not only the latest ones
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
+```
-- `id` (required) - The ID of a project
-- `sha` (required) - The commit SHA
-- `ref` (optional) - Filter by ref name, it can be branch or tag
-- `stage` (optional) - Filter by stage
-- `name` (optional) - Filer by status name, eg. jenkins
-- `all` (optional) - The flag to return all statuses, not only latest ones
+Example response:
```json
[
- {
- "id": 13,
- "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27",
- "ref": "test",
- "status": "success",
- "name": "ci/jenkins",
- "target_url": "http://jenkins/project/url",
- "description": "Jenkins success",
- "created_at": "2015-10-12T09:47:16.250Z",
- "started_at": "2015-10-12T09:47:16.250Z",
- "finished_at": "2015-10-12T09:47:16.262Z",
- "author": {
- "id": 1,
- "username": "admin",
- "email": "admin@local.host",
- "name": "Administrator",
- "blocked": false,
- "created_at": "2012-04-29T08:46:00Z"
- }
- }
+ ...
+
+ {
+ "status" : "pending",
+ "created_at" : "2016-01-19T08:40:25.934Z",
+ "started_at" : null,
+ "name" : "bundler:audit",
+ "allow_failure" : true,
+ "author" : {
+ "username" : "thedude",
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/u/thedude",
+ "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
+ "id" : 28,
+ "name" : "Jeff Lebowski"
+ },
+ "description" : null,
+ "sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8",
+ "target_url" : "https://gitlab.example.com/thedude/gitlab-ce/builds/91",
+ "finished_at" : null,
+ "id" : 91,
+ "ref" : "master"
+ },
+ {
+ "started_at" : null,
+ "name" : "flay",
+ "allow_failure" : false,
+ "status" : "pending",
+ "created_at" : "2016-01-19T08:40:25.832Z",
+ "target_url" : "https://gitlab.example.com/thedude/gitlab-ce/builds/90",
+ "id" : 90,
+ "finished_at" : null,
+ "ref" : "master",
+ "sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8",
+ "author" : {
+ "id" : 28,
+ "name" : "Jeff Lebowski",
+ "username" : "thedude",
+ "web_url" : "https://gitlab.example.com/u/thedude",
+ "state" : "active",
+ "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png"
+ },
+ "description" : null
+ },
+
+ ...
]
```
-## Post the status to commit
+### Post the build status to a commit
-Adds or updates a status of a commit.
+Adds or updates a build status of a commit.
```
POST /projects/:id/statuses/:sha
```
-- `id` (required) - The ID of a project
-- `sha` (required) - The commit SHA
-- `state` (required) - The state of the status. Can be: pending, running, success, failed, canceled
-- `ref` (optional) - The ref (branch or tag) to which the status refers
-- `name` or `context` (optional) - The label to differentiate this status from the status of other systems. Default: "default"
-- `target_url` (optional) - The target URL to associate with this status
-- `description` (optional) - The short description of the status
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project
+| `sha` | string | yes | The commit SHA
+| `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled`
+| `ref` | string | no | The `ref` (branch or tag) to which the status refers
+| `name` or `context` | string | no | The label to differentiate this status from the status of other systems. Default value is `default`
+| `target_url` | string | no | The target URL to associate with this status
+| `description` | string | no | The short description of the status
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
+```
+
+Example response:
```json
{
- "id": 13,
- "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27",
- "ref": "test",
- "status": "success",
- "name": "ci/jenkins",
- "target_url": "http://jenkins/project/url",
- "description": "Jenkins success",
- "created_at": "2015-10-12T09:47:16.250Z",
- "started_at": "2015-10-12T09:47:16.250Z",
- "finished_at": "2015-10-12T09:47:16.262Z",
- "author": {
- "id": 1,
- "username": "admin",
- "email": "admin@local.host",
- "name": "Administrator",
- "blocked": false,
- "created_at": "2012-04-29T08:46:00Z"
- }
+ "author" : {
+ "web_url" : "https://gitlab.example.com/u/thedude",
+ "name" : "Jeff Lebowski",
+ "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
+ "username" : "thedude",
+ "state" : "active",
+ "id" : 28
+ },
+ "name" : "default",
+ "sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8",
+ "status" : "success",
+ "description" : null,
+ "id" : 93,
+ "target_url" : null,
+ "ref" : null,
+ "started_at" : null,
+ "created_at" : "2016-01-19T09:05:50.355Z",
+ "allow_failure" : false,
+ "finished_at" : "2016-01-19T09:05:50.365Z"
}
```
diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md
index 1a5a458905e..3ad836f51b5 100644
--- a/doc/api/deploy_key_multiple_projects.md
+++ b/doc/api/deploy_key_multiple_projects.md
@@ -1,25 +1,29 @@
# Adding deploy keys to multiple projects
-If you want to easily add the same deploy key to multiple projects in the same group, this can be achieved quite easily with the API.
+If you want to easily add the same deploy key to multiple projects in the same
+group, this can be achieved quite easily with the API.
-First, find the ID of the projects you're interested in, by either listing all projects:
+First, find the ID of the projects you're interested in, by either listing all
+projects:
```
-curl --header 'PRIVATE-TOKEN: abcdef' https://gitlab.com/api/v3/projects
+curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects
```
-Or finding the id of a group and then listing all projects in that group:
+Or finding the ID of a group and then listing all projects in that group:
```
-curl --header 'PRIVATE-TOKEN: abcdef' https://gitlab.com/api/v3/groups
+curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups
# For group 1234:
-curl --header 'PRIVATE-TOKEN: abcdef' https://gitlab.com/api/v3/groups/1234
+curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234
```
With those IDs, add the same deploy key to all:
+
```
for project_id in 321 456 987; do
- curl -X POST --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' --header 'PRIVATE-TOKEN: abcdef' https://gitlab.com/api/v3/projects/${project_id}/keys
+ curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" \
+ --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v3/projects/${project_id}/keys
done
```
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index e4492fc609c..9da1fe22e61 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -8,9 +8,15 @@ Get a list of a project's deploy keys.
GET /projects/:id/keys
```
-Parameters:
+| 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/5/keys"
+```
-- `id` (required) - The ID of the project
+Example response:
```json
[
@@ -39,8 +45,16 @@ GET /projects/:id/keys/:key_id
Parameters:
-- `id` (required) - The ID of the project
-- `key_id` (required) - The ID of the deploy key
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+| `key_id` | integer | yes | The ID of the deploy key |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/keys/11"
+```
+
+Example response:
```json
{
@@ -54,17 +68,34 @@ Parameters:
## Add deploy key
Creates a new deploy key for a project.
-If deploy key already exists in another project - it will be joined to project but only if original one was is accessible by same user
+
+If the deploy key already exists in another project, it will be joined to current
+project only if original one was is accessible by the same user.
```
POST /projects/:id/keys
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+| `title` | string | yes | New deploy key's title |
+| `key` | string | yes | New deploy key |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/keys/"
+```
-- `id` (required) - The ID of the project
-- `title` (required) - New deploy key's title
-- `key` (required) - New deploy key
+Example response:
+
+```json
+{
+ "key" : "ssh-rsa AAAA...",
+ "id" : 12,
+ "title" : "My deploy key",
+ "created_at" : "2015-08-29T12:44:31.550Z"
+}
+```
## Delete deploy key
@@ -74,7 +105,26 @@ Delete a deploy key from a project
DELETE /projects/:id/keys/:key_id
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+| `key_id` | integer | yes | The ID of the deploy key |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/keys/13"
+```
-- `id` (required) - The ID of the project
-- `key_id` (required) - The ID of the deploy key
+Example response:
+
+```json
+{
+ "updated_at" : "2015-08-29T12:50:57.259Z",
+ "key" : "ssh-rsa AAAA...",
+ "public" : false,
+ "title" : "My deploy key",
+ "user_id" : null,
+ "created_at" : "2015-08-29T12:50:57.259Z",
+ "fingerprint" : "6a:33:1f:74:51:c0:39:81:79:ec:7a:31:f8:40:20:43",
+ "id" : 13
+}
+```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 808675d8605..d47e79ba47f 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -33,6 +33,7 @@ GET /groups/:id/projects
Parameters:
- `archived` (optional) - if passed, limit by archived status
+- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
diff --git a/doc/api/issues.md b/doc/api/issues.md
index d407bc35d79..9e704648b25 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -1,9 +1,20 @@
# Issues
+Every API call to issues must be authenticated.
+
+If a user is not a member of a project and the project is private, a `GET`
+request on that project will result to a `404` status code.
+
+## Issues pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](README.md#pagination).
+
## List issues
-Get all issues created by authenticated user. This function takes pagination parameters
-`page` and `per_page` to restrict the list of issues.
+Get all issues created by the authenticated user.
```
GET /issues
@@ -14,81 +25,65 @@ GET /issues?labels=foo,bar
GET /issues?labels=foo,bar&state=opened
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
+| `labels` | string | no | Comma-separated list of label names |
+| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues
+```
-- `state` (optional) - Return `all` issues or just those that are `opened` or `closed`
-- `labels` (optional) - Comma-separated list of label names
-- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
+Example response:
```json
[
- {
- "id": 43,
- "iid": 3,
- "project_id": 8,
- "title": "4xx/5xx pages",
- "description": "",
- "labels": [],
- "milestone": null,
- "assignee": null,
- "author": {
- "id": 1,
- "username": "john_smith",
- "email": "john@example.com",
- "name": "John Smith",
- "state": "active",
- "created_at": "2012-05-23T08:00:58Z"
- },
- "state": "closed",
- "updated_at": "2012-07-02T17:53:12Z",
- "created_at": "2012-07-02T17:53:12Z"
- },
- {
- "id": 42,
- "iid": 4,
- "project_id": 8,
- "title": "Add user settings",
- "description": "",
- "labels": [
- "feature"
- ],
- "milestone": {
- "id": 1,
- "title": "v1.0",
- "description": "",
- "due_date": "2012-07-20",
- "state": "reopened",
- "updated_at": "2012-07-04T13:42:48Z",
- "created_at": "2012-07-04T13:42:48Z"
- },
- "assignee": {
- "id": 2,
- "username": "jack_smith",
- "email": "jack@example.com",
- "name": "Jack Smith",
- "state": "active",
- "created_at": "2012-05-23T08:01:01Z"
- },
- "author": {
- "id": 1,
- "username": "john_smith",
- "email": "john@example.com",
- "name": "John Smith",
- "state": "active",
- "created_at": "2012-05-23T08:00:58Z"
- },
- "state": "opened",
- "updated_at": "2012-07-12T13:43:19Z",
- "created_at": "2012-06-28T12:58:06Z"
- }
+ {
+ "state" : "opened",
+ "description" : "Ratione dolores corrupti mollitia soluta quia.",
+ "author" : {
+ "state" : "active",
+ "id" : 18,
+ "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "username" : "eileen.lowe"
+ },
+ "milestone" : {
+ "project_id" : 1,
+ "description" : "Ducimus nam enim ex consequatur cumque ratione.",
+ "state" : "closed",
+ "due_date" : null,
+ "iid" : 2,
+ "created_at" : "2016-01-04T15:31:39.996Z",
+ "title" : "v4.0",
+ "id" : 17,
+ "updated_at" : "2016-01-04T15:31:39.996Z"
+ },
+ "project_id" : 1,
+ "assignee" : {
+ "state" : "active",
+ "id" : 1,
+ "name" : "Administrator",
+ "web_url" : "https://gitlab.example.com/u/root",
+ "avatar_url" : null,
+ "username" : "root"
+ },
+ "updated_at" : "2016-01-04T15:31:51.081Z",
+ "id" : 76,
+ "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
+ "created_at" : "2016-01-04T15:31:51.081Z",
+ "iid" : 6,
+ "labels" : []
+ },
]
```
## List project issues
-Get a list of project issues. This function accepts pagination parameters `page` and `per_page`
-to return the list of project issues.
+Get a list of a project's issues.
```
GET /projects/:id/issues
@@ -102,67 +97,123 @@ GET /projects/:id/issues?milestone=1.0.0&state=opened
GET /projects/:id/issues?iid=42
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `iid` | integer | no | Return the issue having the given `iid` |
+| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
+| `labels` | string | no | Comma-separated list of label names |
+| `milestone` | string| no | The milestone title |
+| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues
+```
+
+Example response:
-- `id` (required) - The ID of a project
-- `iid` (optional) - Return the issue having the given `iid`
-- `state` (optional) - Return `all` issues or just those that are `opened` or `closed`
-- `labels` (optional) - Comma-separated list of label names
-- `milestone` (optional) - Milestone title
-- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
+```json
+[
+ {
+ "project_id" : 4,
+ "milestone" : {
+ "due_date" : null,
+ "project_id" : 4,
+ "state" : "closed",
+ "description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.",
+ "iid" : 3,
+ "id" : 11,
+ "title" : "v3.0",
+ "created_at" : "2016-01-04T15:31:39.788Z",
+ "updated_at" : "2016-01-04T15:31:39.788Z"
+ },
+ "author" : {
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/u/root",
+ "avatar_url" : null,
+ "username" : "root",
+ "id" : 1,
+ "name" : "Administrator"
+ },
+ "description" : "Omnis vero earum sunt corporis dolor et placeat.",
+ "state" : "closed",
+ "iid" : 1,
+ "assignee" : {
+ "avatar_url" : null,
+ "web_url" : "https://gitlab.example.com/u/lennie",
+ "state" : "active",
+ "username" : "lennie",
+ "id" : 9,
+ "name" : "Dr. Luella Kovacek"
+ },
+ "labels" : [],
+ "id" : 41,
+ "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
+ "updated_at" : "2016-01-04T15:31:46.176Z",
+ "created_at" : "2016-01-04T15:31:46.176Z"
+ }
+]
+```
## Single issue
-Gets a single project issue.
+Get a single project issue.
```
GET /projects/:id/issues/:issue_id
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id`| integer | yes | The ID of a project's issue |
-- `id` (required) - The ID of a project
-- `issue_id` (required) - The ID of a project issue
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41
+```
+
+Example response:
```json
{
- "id": 42,
- "iid": 3,
- "project_id": 8,
- "title": "Add user settings",
- "description": "",
- "labels": [
- "feature"
- ],
- "milestone": {
- "id": 1,
- "title": "v1.0",
- "description": "",
- "due_date": "2012-07-20",
- "state": "closed",
- "updated_at": "2012-07-04T13:42:48Z",
- "created_at": "2012-07-04T13:42:48Z"
- },
- "assignee": {
- "id": 2,
- "username": "jack_smith",
- "email": "jack@example.com",
- "name": "Jack Smith",
- "state": "active",
- "created_at": "2012-05-23T08:01:01Z"
- },
- "author": {
- "id": 1,
- "username": "john_smith",
- "email": "john@example.com",
- "name": "John Smith",
- "state": "active",
- "created_at": "2012-05-23T08:00:58Z"
- },
- "state": "opened",
- "updated_at": "2012-07-12T13:43:19Z",
- "created_at": "2012-06-28T12:58:06Z"
+ "project_id" : 4,
+ "milestone" : {
+ "due_date" : null,
+ "project_id" : 4,
+ "state" : "closed",
+ "description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.",
+ "iid" : 3,
+ "id" : 11,
+ "title" : "v3.0",
+ "created_at" : "2016-01-04T15:31:39.788Z",
+ "updated_at" : "2016-01-04T15:31:39.788Z"
+ },
+ "author" : {
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/u/root",
+ "avatar_url" : null,
+ "username" : "root",
+ "id" : 1,
+ "name" : "Administrator"
+ },
+ "description" : "Omnis vero earum sunt corporis dolor et placeat.",
+ "state" : "closed",
+ "iid" : 1,
+ "assignee" : {
+ "avatar_url" : null,
+ "web_url" : "https://gitlab.example.com/u/lennie",
+ "state" : "active",
+ "username" : "lennie",
+ "id" : 9,
+ "name" : "Dr. Luella Kovacek"
+ },
+ "labels" : [],
+ "id" : 41,
+ "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
+ "updated_at" : "2016-01-04T15:31:46.176Z",
+ "created_at" : "2016-01-04T15:31:46.176Z"
}
```
@@ -170,57 +221,122 @@ Parameters:
Creates a new project issue.
+If the operation is successful, a status code of `200` and the newly-created
+issue is returned. If an error occurs, an error number and a message explaining
+the reason is returned.
+
```
POST /projects/:id/issues
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `title` | string | yes | The title of an issue |
+| `description` | string | no | The description of an issue |
+| `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 |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
+```
-- `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) - Comma-separated label names for an issue
+Example response:
-If the operation is successful, 200 and the newly created issue is returned.
-If an error occurs, an error number and a message explaining the reason is returned.
+```json
+{
+ "project_id" : 4,
+ "id" : 84,
+ "created_at" : "2016-01-07T12:44:33.959Z",
+ "iid" : 14,
+ "title" : "Issues with auth",
+ "state" : "opened",
+ "assignee" : null,
+ "labels" : [
+ "bug"
+ ],
+ "author" : {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+ "id" : 18,
+ "username" : "eileen.lowe"
+ },
+ "description" : null,
+ "updated_at" : "2016-01-07T12:44:33.959Z",
+ "milestone" : null
+}
+```
## Edit issue
-Updates an existing project issue. This function is also used to mark an issue as closed.
+Updates an existing project issue. This call is also used to mark an issue as
+closed.
+
+If the operation is successful, a code of `200` and the updated issue is
+returned. If an error occurs, an error number and a message explaining the
+reason is returned.
```
PUT /projects/:id/issues/:issue_id
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+| `title` | string | no | The title of an issue |
+| `description` | string | no | The description of an issue |
+| `assignee_id` | integer | no | The ID of a user to assign the issue to |
+| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
+| `labels` | string | no | Comma-separated label names for an issue |
+| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
+
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
+```
-- `id` (required) - The ID of a project
-- `issue_id` (required) - The ID of a project's issue
-- `title` (optional) - 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) - Comma-separated label names for an issue
-- `state_event` (optional) - The state event of an issue ('close' to close issue and 'reopen' to reopen it)
+Example response:
-If the operation is successful, 200 and the updated issue is returned.
-If an error occurs, an error number and a message explaining the reason is returned.
+```json
+{
+ "created_at" : "2016-01-07T12:46:01.410Z",
+ "author" : {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "username" : "eileen.lowe",
+ "id" : 18,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/u/eileen.lowe"
+ },
+ "state" : "closed",
+ "title" : "Issues with auth",
+ "project_id" : 4,
+ "description" : null,
+ "updated_at" : "2016-01-07T12:55:16.213Z",
+ "iid" : 15,
+ "labels" : [
+ "bug"
+ ],
+ "id" : 85,
+ "assignee" : null,
+ "milestone" : null
+}
+```
## Delete existing issue (**Deprecated**)
-The function 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 parameter `state_event` set to `close`.
+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.
```
DELETE /projects/:id/issues/:issue_id
```
-Parameters:
-
-- `id` (required) - The project ID
-- `issue_id` (required) - The ID of the issue
-
## Comments on issues
-Comments are done via the notes resource.
+Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/labels.md b/doc/api/labels.md
index de41f35d284..6496ffe9fd1 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -2,83 +2,153 @@
## List labels
-Get all labels for given project.
+Get all labels for a given project.
```
GET /projects/:id/labels
```
+| 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
+```
+
+Example response:
+
```json
[
- {
- "name": "Awesome",
- "color": "#DD10AA"
- },
- {
- "name": "Documentation",
- "color": "#1E80DD"
- },
- {
- "name": "Feature",
- "color": "#11FF22"
- },
- {
- "name": "Bug",
- "color": "#EE1122"
- }
+ {
+ "name" : "bug",
+ "color" : "#d9534f"
+ },
+ {
+ "color" : "#d9534f",
+ "name" : "confirmed"
+ },
+ {
+ "name" : "critical",
+ "color" : "#d9534f"
+ },
+ {
+ "color" : "#428bca",
+ "name" : "discussion"
+ },
+ {
+ "name" : "documentation",
+ "color" : "#f0ad4e"
+ },
+ {
+ "color" : "#5cb85c",
+ "name" : "enhancement"
+ },
+ {
+ "color" : "#428bca",
+ "name" : "suggestion"
+ },
+ {
+ "color" : "#f0ad4e",
+ "name" : "support"
+ }
]
```
## Create a new label
-Creates a new label for given repository with given name and color.
+Creates a new label for the given repository with the given name and color.
+
+It returns 200 if the label was successfully created, 400 for wrong parameters
+and 409 if the label already exists.
```
POST /projects/:id/labels
```
-Parameters:
+| 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 |
+
+```bash
+curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+```
-- `id` (required) - The ID of a project
-- `name` (required) - The name of the label
-- `color` (required) - Color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)
+Example response:
-It returns 200 and the newly created label, if the operation succeeds.
-If the label already exists, 409 and an error message is returned.
-If label parameters are invalid, 400 and an explaining error message is returned.
+```json
+{
+ "name" : "feature",
+ "color" : "#5843AD"
+}
+```
## Delete a label
-Deletes a label given by its name.
+Deletes a label with a given name.
+
+It returns 200 if the label was successfully deleted, 400 for wrong parameters
+and 404 if the label does not exist.
+In case of an error, an additional error message is returned.
```
DELETE /projects/:id/labels
```
-- `id` (required) - The ID of a project
-- `name` (required) - The name of the label to be deleted
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+| `name` | string | yes | The name of the label |
-It returns 200 if the label successfully was deleted, 400 for wrong parameters
-and 404 if the label does not exist.
-In case of an error, additionally an error message is returned.
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
+```
+
+Example response:
+
+```json
+{
+ "title" : "feature",
+ "color" : "#5843AD",
+ "updated_at" : "2015-11-03T21:22:30.737Z",
+ "template" : false,
+ "project_id" : 1,
+ "created_at" : "2015-11-03T21:22:30.737Z",
+ "id" : 9
+}
+```
## Edit an existing label
-Updates an existing label with new name or now color. At least one parameter
+Updates an existing label with new name or new color. At least one parameter
is required, to update the label.
+It returns 200 if the label was successfully deleted, 400 for wrong parameters
+and 404 if the label does not exist.
+In case of an error, an additional error message is returned.
+
```
PUT /projects/:id/labels
```
-Parameters:
+| 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 |
+
+```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"
+```
-- `id` (required) - The ID of a project
-- `name` (required) - The name of the existing label
-- `new_name` (optional) - The new name of the label
-- `color` (optional) - New color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)
+Example response:
-On success, this method returns 200 with the updated label.
-If required parameters are missing or parameters are invalid, 400 is returned.
-If the label to be updated is missing, 404 is returned.
-In case of an error, additionally an error message is returned.
+```json
+{
+ "color" : "#8E44AD",
+ "name" : "docs"
+}
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 8bc0a67067a..5c527d55481 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -2,7 +2,7 @@
## List merge requests
-Get all merge requests for this project.
+Get all merge requests for this project.
The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
@@ -49,8 +49,24 @@ Parameters:
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
},
+ "source_project_id": "2",
+ "target_project_id": "3",
+ "labels": [ ],
"description":"fixed login page css paddings",
- "work_in_progress": false
+ "work_in_progress": false,
+ "milestone": {
+ "id": 5,
+ "iid": 1,
+ "project_id": 3,
+ "title": "v2.0",
+ "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
+ "state": "closed",
+ "created_at": "2015-02-02T19:49:26.013Z",
+ "updated_at": "2015-02-02T19:49:26.013Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": true,
+ "merge_status": "can_be_merged"
}
]
```
@@ -60,7 +76,7 @@ Parameters:
Shows information about a single merge request.
```
-GET /projects/:id/merge_request/:merge_request_id
+GET /projects/:id/merge_requests/:merge_request_id
```
Parameters:
@@ -95,8 +111,24 @@ Parameters:
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
},
+ "source_project_id": "2",
+ "target_project_id": "3",
+ "labels": [ ],
"description":"fixed login page css paddings",
- "work_in_progress": false
+ "work_in_progress": false,
+ "milestone": {
+ "id": 5,
+ "iid": 1,
+ "project_id": 3,
+ "title": "v2.0",
+ "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
+ "state": "closed",
+ "created_at": "2015-02-02T19:49:26.013Z",
+ "updated_at": "2015-02-02T19:49:26.013Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": true,
+ "merge_status": "can_be_merged"
}
```
@@ -105,7 +137,7 @@ Parameters:
Get a list of merge request commits.
```
-GET /projects/:id/merge_request/:merge_request_id/commits
+GET /projects/:id/merge_requests/:merge_request_id/commits
```
Parameters:
@@ -142,7 +174,7 @@ Parameters:
Shows information about the merge request including its files and changes.
```
-GET /projects/:id/merge_request/:merge_request_id/changes
+GET /projects/:id/merge_requests/:merge_request_id/changes
```
Parameters:
@@ -156,8 +188,6 @@ Parameters:
"iid": 1,
"project_id": 4,
"title": "Blanditiis beatae suscipit hic assumenda et molestias nisi asperiores repellat et.",
- "description": "Qui voluptatibus placeat ipsa alias quasi. Deleniti rem ut sint. Optio velit qui distinctio.",
- "work_in_progress": false,
"state": "reopened",
"created_at": "2015-02-02T19:49:39.159Z",
"updated_at": "2015-02-02T20:08:49.959Z",
@@ -182,6 +212,8 @@ Parameters:
"source_project_id": 4,
"target_project_id": 4,
"labels": [ ],
+ "description": "Qui voluptatibus placeat ipsa alias quasi. Deleniti rem ut sint. Optio velit qui distinctio.",
+ "work_in_progress": false,
"milestone": {
"id": 5,
"iid": 1,
@@ -193,6 +225,8 @@ Parameters:
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
+ "merge_when_build_succeeds": true,
+ "merge_status": "can_be_merged",
"changes": [
{
"old_path": "VERSION",
@@ -225,6 +259,7 @@ Parameters:
- `description` (optional) - Description of MR
- `target_project_id` (optional) - The target project (numeric id)
- `labels` (optional) - Labels for MR as a comma-separated list
+- `milestone_id` (optional) - Milestone ID
```json
{
@@ -252,7 +287,24 @@ Parameters:
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
},
- "description":"fixed login page css paddings"
+ "source_project_id": 4,
+ "target_project_id": 4,
+ "labels": [ ],
+ "description":"fixed login page css paddings",
+ "work_in_progress": false,
+ "milestone": {
+ "id": 5,
+ "iid": 1,
+ "project_id": 4,
+ "title": "v2.0",
+ "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
+ "state": "closed",
+ "created_at": "2015-02-02T19:49:26.013Z",
+ "updated_at": "2015-02-02T19:49:26.013Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": true,
+ "merge_status": "can_be_merged"
}
```
@@ -264,7 +316,7 @@ If an error occurs, an error number and a message explaining the reason is retur
Updates an existing merge request. You can change the target branch, title, or even close the MR.
```
-PUT /projects/:id/merge_request/:merge_request_id
+PUT /projects/:id/merge_requests/:merge_request_id
```
Parameters:
@@ -277,6 +329,7 @@ Parameters:
- `description` - Description of MR
- `state_event` - New state (close|reopen|merge)
- `labels` (optional) - Labels for MR as a comma-separated list
+- `milestone_id` (optional) - Milestone ID
```json
{
@@ -284,7 +337,6 @@ Parameters:
"target_branch": "master",
"project_id": 3,
"title": "test1",
- "description": "description1",
"state": "opened",
"upvotes": 0,
"downvotes": 0,
@@ -303,7 +355,25 @@ Parameters:
"name": "Administrator",
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
- }
+ },
+ "source_project_id": 4,
+ "target_project_id": 4,
+ "labels": [ ],
+ "description": "description1",
+ "work_in_progress": false,
+ "milestone": {
+ "id": 5,
+ "iid": 1,
+ "project_id": 4,
+ "title": "v2.0",
+ "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
+ "state": "closed",
+ "created_at": "2015-02-02T19:49:26.013Z",
+ "updated_at": "2015-02-02T19:49:26.013Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": true,
+ "merge_status": "can_be_merged"
}
```
@@ -323,7 +393,7 @@ If merge request is already merged or closed - you get 405 and error message 'Me
If you don't have permissions to accept this merge request - you'll get a 401
```
-PUT /projects/:id/merge_request/:merge_request_id/merge
+PUT /projects/:id/merge_requests/:merge_request_id/merge
```
Parameters:
@@ -359,7 +429,25 @@ Parameters:
"name": "Administrator",
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
- }
+ },
+ "source_project_id": 4,
+ "target_project_id": 4,
+ "labels": [ ],
+ "description":"fixed login page css paddings",
+ "work_in_progress": false,
+ "milestone": {
+ "id": 5,
+ "iid": 1,
+ "project_id": 4,
+ "title": "v2.0",
+ "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
+ "state": "closed",
+ "created_at": "2015-02-02T19:49:26.013Z",
+ "updated_at": "2015-02-02T19:49:26.013Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": true,
+ "merge_status": "can_be_merged"
}
```
@@ -373,7 +461,7 @@ If the merge request is already merged or closed - you get 405 and error message
In case the merge request is not set to be merged when the build succeeds, you'll also get a 406 error.
```
-PUT /projects/:id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds
+PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds
```
Parameters:
@@ -387,7 +475,7 @@ Parameters:
"source_branch": "test1",
"project_id": 3,
"title": "test1",
- "state": "merged",
+ "state": "opened",
"upvotes": 0,
"downvotes": 0,
"author": {
@@ -405,70 +493,90 @@ Parameters:
"name": "Administrator",
"state": "active",
"created_at": "2012-04-29T08:46:00Z"
- }
+ },
+ "source_project_id": 4,
+ "target_project_id": 4,
+ "labels": [ ],
+ "description":"fixed login page css paddings",
+ "work_in_progress": false,
+ "milestone": {
+ "id": 5,
+ "iid": 1,
+ "project_id": 4,
+ "title": "v2.0",
+ "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
+ "state": "closed",
+ "created_at": "2015-02-02T19:49:26.013Z",
+ "updated_at": "2015-02-02T19:49:26.013Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": true,
+ "merge_status": "can_be_merged"
}
```
-## Post comment to MR
+## Comments on merge requests
-Adds a comment to a merge request.
+Comments are done via the [notes](notes.md) resource.
-```
-POST /projects/:id/merge_request/:merge_request_id/comments
-```
+## List issues that will close on merge
-Parameters:
-
-- `id` (required) - The ID of a project
-- `merge_request_id` (required) - ID of merge request
-- `note` (required) - Text of comment
+Get all the issues that would be closed by merging the provided merge request.
-```json
-{
- "note": "text1"
-}
+```
+GET /projects/:id/merge_requests/:merge_request_id/closes_issues
```
-## Get the comments on a MR
-
-Gets all the comments associated with a merge request.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
```
-GET /projects/:id/merge_request/:merge_request_id/comments
-```
-
-Parameters:
-- `id` (required) - The ID of a project
-- `merge_request_id` (required) - ID of merge request
+Example response:
```json
[
- {
- "note": "this is the 1st comment on the 2merge merge request",
- "author": {
- "id": 11,
- "username": "admin",
- "email": "admin@example.com",
- "name": "Administrator",
- "state": "active",
- "created_at": "2014-03-06T08:17:35.000Z"
- }
- },
- {
- "note": "Status changed to closed",
- "author": {
- "id": 11,
- "username": "admin",
- "email": "admin@example.com",
- "name": "Administrator",
- "state": "active",
- "created_at": "2014-03-06T08:17:35.000Z"
- }
- }
+ {
+ "state" : "opened",
+ "description" : "Ratione dolores corrupti mollitia soluta quia.",
+ "author" : {
+ "state" : "active",
+ "id" : 18,
+ "web_url" : "https://gitlab.example.com/u/eileen.lowe",
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "username" : "eileen.lowe"
+ },
+ "milestone" : {
+ "project_id" : 1,
+ "description" : "Ducimus nam enim ex consequatur cumque ratione.",
+ "state" : "closed",
+ "due_date" : null,
+ "iid" : 2,
+ "created_at" : "2016-01-04T15:31:39.996Z",
+ "title" : "v4.0",
+ "id" : 17,
+ "updated_at" : "2016-01-04T15:31:39.996Z"
+ },
+ "project_id" : 1,
+ "assignee" : {
+ "state" : "active",
+ "id" : 1,
+ "name" : "Administrator",
+ "web_url" : "https://gitlab.example.com/u/root",
+ "avatar_url" : null,
+ "username" : "root"
+ },
+ "updated_at" : "2016-01-04T15:31:51.081Z",
+ "id" : 76,
+ "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
+ "created_at" : "2016-01-04T15:31:51.081Z",
+ "iid" : 6,
+ "labels" : []
+ },
]
```
-
-## Comments on merge requets
-
-Comments are done via the notes resource.
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 7b3238441f6..42d9ce3d391 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -1,13 +1,29 @@
# Namespaces
+Usernames and groupnames fall under a special category called namespaces.
+
+For users and groups supported API calls see the [users](users.md) and
+[groups](groups.md) documentation respectively.
+
+[Pagination](README.md#pagination) is used.
+
## List namespaces
-Get a list of namespaces. (As user: my namespaces, as admin: all namespaces)
+Get a list of the namespaces of the authenticated user. If the user is an
+administrator, a list of all namespaces in the GitLab instance is shown.
```
GET /namespaces
```
+Example request:
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces
+```
+
+Example response:
+
```json
[
{
@@ -23,22 +39,32 @@ GET /namespaces
]
```
-You can search for namespaces by name or path, see below.
-
## Search for namespace
-Get all namespaces that match your string in their name or path.
+Get all namespaces that match a string in their name or path.
```
GET /namespaces?search=foobar
```
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `search` | string | no | Returns a list of namespaces the user is authorized to see based on the search criteria |
+
+Example request:
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter
+```
+
+Example response:
+
```json
[
{
- "id": 1,
- "path": "user1",
- "kind": "user"
+ "id": 4,
+ "path": "twitter",
+ "kind": "group"
}
]
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 0ca81ffd49e..3703f4b327a 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -29,6 +29,7 @@ GET /projects
Parameters:
- `archived` (optional) - if passed, limit by archived status
+- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
@@ -76,7 +77,12 @@ Parameters:
"updated_at": "2013-09-30T13: 46: 02Z"
},
"archived": false,
- "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png"
+ "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02",
+ "public_builds": true
},
{
"id": 6,
@@ -129,7 +135,12 @@ Parameters:
}
},
"archived": false,
- "avatar_url": null
+ "avatar_url": null,
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02",
+ "public_builds": true
}
]
```
@@ -145,6 +156,7 @@ GET /projects/owned
Parameters:
- `archived` (optional) - if passed, limit by archived status
+- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
@@ -160,6 +172,7 @@ GET /projects/starred
Parameters:
- `archived` (optional) - if passed, limit by archived status
+- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
@@ -175,6 +188,7 @@ GET /projects/all
Parameters:
- `archived` (optional) - if passed, limit by archived status
+- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
@@ -244,7 +258,11 @@ Parameters:
}
},
"archived": false,
- "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png"
+ "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"
}
```
@@ -409,6 +427,7 @@ Parameters:
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
- `import_url` (optional)
+- `public_builds` (optional)
### Create project for user
@@ -431,6 +450,7 @@ Parameters:
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
- `import_url` (optional)
+- `public_builds` (optional)
### Edit project
@@ -454,6 +474,7 @@ Parameters:
- `snippets_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
+- `public_builds` (optional)
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
@@ -482,6 +503,34 @@ Parameters:
- `id` (required) - The ID of a project
+## Uploads
+
+### Upload a file
+
+Uploads a file to the specified project to be used in an issue or merge request description, or a comment.
+
+```
+POST /projects/:id/uploads
+```
+
+Parameters:
+
+- `id` (required) - The ID of the project
+- `file` (required) - The file to be uploaded
+
+```json
+{
+ "alt": "dk",
+ "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png",
+ "is_image": true,
+ "markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)"
+}
+```
+
+**Note**: The returned `url` is relative to the project path.
+In Markdown contexts, the link is automatically expanded when the format in `markdown` is used.
+
+
## Team members
### List project team members
@@ -570,6 +619,20 @@ Revoking team membership for a user who is not currently a team member is consid
Please note that the returned JSON currently differs slightly. Thus you should not
rely on the returned JSON structure.
+### Share project with group
+
+Allow to share project with group.
+
+```
+POST /projects/:id/share
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `group_id` (required) - The ID of a group
+- `group_access` (required) - Level of permissions for sharing
+
## Hooks
Also called Project Hooks and Webhooks.
diff --git a/doc/api/runners.md b/doc/api/runners.md
new file mode 100644
index 00000000000..cc6c6b7cb2f
--- /dev/null
+++ b/doc/api/runners.md
@@ -0,0 +1,322 @@
+# Runners API
+
+> [Introduced][ce-2640] in GitLab 8.5
+
+[ce-2640]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2640
+
+## List owned runners
+
+Get a list of specific runners available to the user.
+
+```
+GET /runners
+GET /runners?scope=active
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `scope` | string | no | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided |
+
+```
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners"
+```
+
+Example response:
+
+```json
+[
+ {
+ "active": true,
+ "description": "test-1-20150125",
+ "id": 6,
+ "is_shared": false,
+ "name": null
+ },
+ {
+ "active": true,
+ "description": "test-2-20150125",
+ "id": 8,
+ "is_shared": false,
+ "name": null
+ }
+]
+```
+
+## List all runners
+
+Get a list of all runners in the GitLab instance (specific and shared). Access
+is restricted to users with `admin` privileges.
+
+```
+GET /runners/all
+GET /runners/all?scope=online
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `scope` | string | no | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided |
+
+```
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all"
+```
+
+Example response:
+
+```json
+[
+ {
+ "active": true,
+ "description": "shared-runner-1",
+ "id": 1,
+ "is_shared": true,
+ "name": null
+ },
+ {
+ "active": true,
+ "description": "shared-runner-2",
+ "id": 3,
+ "is_shared": true,
+ "name": null
+ },
+ {
+ "active": true,
+ "description": "test-1-20150125",
+ "id": 6,
+ "is_shared": false,
+ "name": null
+ },
+ {
+ "active": true,
+ "description": "test-2-20150125",
+ "id": 8,
+ "is_shared": false,
+ "name": null
+ }
+]
+```
+
+## Get runner's details
+
+Get details of a runner.
+
+```
+GET /runners/:id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a runner |
+
+```
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
+```
+
+Example response:
+
+```json
+{
+ "active": true,
+ "architecture": null,
+ "description": "test-1-20150125",
+ "id": 6,
+ "is_shared": false,
+ "contacted_at": "2016-01-25T16:39:48.066Z",
+ "name": null,
+ "platform": null,
+ "projects": [
+ {
+ "id": 1,
+ "name": "GitLab Community Edition",
+ "name_with_namespace": "GitLab.org / GitLab Community Edition",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ }
+ ],
+ "token": "205086a8e3b9a2b818ffac9b89d102",
+ "revision": null,
+ "tag_list": [
+ "ruby",
+ "mysql"
+ ],
+ "version": null
+}
+```
+
+## Update runner's details
+
+Update details of a runner.
+
+```
+PUT /runners/:id
+```
+
+| Attribute | Type | Required | Description |
+|---------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a runner |
+| `description` | string | no | The description of a runner |
+| `active` | boolean | no | The state of a runner; can be set to `true` or `false` |
+| `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner |
+
+```
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" -F "description=test-1-20150125-test" -F "tag_list=ruby,mysql,tag1,tag2"
+```
+
+Example response:
+
+```json
+{
+ "active": true,
+ "architecture": null,
+ "description": "test-1-20150125-test",
+ "id": 6,
+ "is_shared": false,
+ "contacted_at": "2016-01-25T16:39:48.066Z",
+ "name": null,
+ "platform": null,
+ "projects": [
+ {
+ "id": 1,
+ "name": "GitLab Community Edition",
+ "name_with_namespace": "GitLab.org / GitLab Community Edition",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ }
+ ],
+ "token": "205086a8e3b9a2b818ffac9b89d102",
+ "revision": null,
+ "tag_list": [
+ "ruby",
+ "mysql",
+ "tag1",
+ "tag2"
+ ],
+ "version": null
+}
+```
+
+## Remove a runner
+
+Remove a runner.
+
+```
+DELETE /runners/:id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a runner |
+
+```
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
+```
+
+Example response:
+
+```json
+{
+ "active": true,
+ "description": "test-1-20150125-test",
+ "id": 6,
+ "is_shared": false,
+ "name": null,
+}
+```
+
+## List project's runners
+
+List all runners (specific and shared) available in the project. Shared runners
+are listed if at least one shared runner is defined **and** shared runners
+usage is enabled in the project's settings.
+
+```
+GET /projects/:id/runners
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners"
+```
+
+Example response:
+
+```json
+[
+ {
+ "active": true,
+ "description": "test-2-20150125",
+ "id": 8,
+ "is_shared": false,
+ "name": null
+ },
+ {
+ "active": true,
+ "description": "development_runner",
+ "id": 5,
+ "is_shared": true,
+ "name": null
+ }
+]
+```
+
+## Enable a runner in project
+
+Enable an available specific runner in the project.
+
+```
+POST /projects/:id/runners
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `runner_id` | integer | yes | The ID of a runner |
+
+```
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners" -F "runner_id=9"
+```
+
+Example response:
+
+```json
+{
+ "active": true,
+ "description": "test-2016-02-01",
+ "id": 9,
+ "is_shared": false,
+ "name": null
+}
+```
+
+## Disable a runner from project
+
+Disable a specific runner from the project. It works only if the project isn't
+the only project associated with the specified runner. If so, an error is
+returned. Use the [Remove a runner](#remove-a-runner) call instead.
+
+```
+DELETE /projects/:id/runners/:runner_id
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `runner_id` | integer | yes | The ID of a runner |
+
+```
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners/9"
+```
+
+Example response:
+
+```json
+{
+ "active": true,
+ "description": "test-2016-02-01",
+ "id": 9,
+ "is_shared": false,
+ "name": null
+}
+```
diff --git a/doc/api/session.md b/doc/api/session.md
index 47c1c8a7a49..71e93d0bb0a 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -1,39 +1,47 @@
# Session
-Login to get private token
+You can login with both GitLab and LDAP credentials in order to obtain the
+private token.
```
POST /session
```
-Parameters:
+| Attribute | Type | Required | Description |
+| ---------- | ------- | -------- | -------- |
+| `login` | string | yes | The username of the user|
+| `email` | string | yes if login is not provided | The email of the user |
+| `password` | string | yes | The password of the user |
-- `login` (required) - The login of user
-- `email` (required if login missing) - The email of user
-- `password` (required) - Valid password
-
-**You can login with both GitLab and LDAP credentials now**
+```bash
+curl -X POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd"
+```
+Example response:
```json
{
- "id": 1,
- "username": "john_smith",
- "email": "john@example.com",
"name": "John Smith",
- "private_token": "dd34asd13as",
- "blocked": false,
- "created_at": "2012-05-23T08:00:58Z",
+ "username": "john_smith",
+ "id": 32,
+ "state": "active",
+ "avatar_url": null,
+ "created_at": "2015-01-29T21:07:19.440Z",
+ "is_admin": true,
"bio": null,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": "",
- "dark_scheme": false,
+ "email": "john@example.com",
"theme_id": 1,
- "is_admin": false,
+ "color_scheme_id": 1,
+ "projects_limit": 10,
+ "current_sign_in_at": "2015-07-07T07:10:58.392Z",
+ "identities": [],
"can_create_group": true,
- "can_create_team": true,
- "can_create_project": true
+ "can_create_project": true,
+ "two_factor_enabled": false,
+ "private_token": "9koXpg98eAheJpvBs5tK"
}
```
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 96867c67915..001de76c7af 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -1,67 +1,77 @@
# Application settings
-This API allows you to read and modify GitLab instance application settings.
+These API calls allow you to read and modify GitLab instance application
+settings as appear in `/admin/application_settings`. You have to be an
+administrator in order to perform this action.
+## Get current application settings
-## Get current application settings:
+List the current application settings of the GitLab instance.
```
GET /application/settings
```
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings
+```
+
+Example response:
+
```json
{
- "id": 1,
- "default_projects_limit": 10,
- "signup_enabled": true,
- "signin_enabled": true,
- "gravatar_enabled": true,
- "sign_in_text": "",
- "created_at": "2015-06-12T15:51:55.432Z",
- "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,
- "default_project_visibility": 0,
- "default_snippet_visibility": 0,
- "restricted_signup_domains": [],
- "user_oauth_applications": true,
- "after_sign_out_path": ""
+ "default_projects_limit" : 10,
+ "signup_enabled" : true,
+ "id" : 1,
+ "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,
+ "updated_at" : "2016-01-04T15:44:55.176Z",
+ "session_expire_delay" : 10080,
+ "home_page_url" : null,
+ "default_snippet_visibility" : 0,
+ "restricted_signup_domains" : [],
+ "created_at" : "2016-01-04T15:44:55.176Z",
+ "default_project_visibility" : 0,
+ "gravatar_enabled" : true,
+ "sign_in_text" : null
}
```
-## Change application settings:
-
-
+## Change application settings
```
PUT /application/settings
```
-Parameters:
-
-- `default_projects_limit` - project limit per user
-- `signup_enabled` - enable registration
-- `signin_enabled` - enable login via GitLab account
-- `gravatar_enabled` - enable gravatar
-- `sign_in_text` - text on login page
-- `home_page_url` - redirect to this URL when not logged in
-- `default_branch_protection` - determine if developers can push to master
-- `twitter_sharing_enabled` - allow users to share project creation in twitter
-- `restricted_visibility_levels` - restrict certain visibility levels
-- `max_attachment_size` - limit attachment size
-- `session_expire_delay` - session lifetime
-- `default_project_visibility` - what visibility level new project receives
-- `default_snippet_visibility` - what visibility level new snippet receives
-- `restricted_signup_domains` - force people to use only corporate emails for signup
-- `user_oauth_applications` - allow users to create oauth applications
-- `after_sign_out_path` - where redirect user after logout
+| Attribute | Type | Required | Description |
+| --------- | ---- | :------: | ----------- |
+| `default_projects_limit` | integer | no | Project limit per user. Default is `10` |
+| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
+| `signin_enabled` | boolean | no | Enable login via a GitLab account. Default is `true`. |
+| `gravatar_enabled` | boolean | no | Enable Gravatar |
+| `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 |
+| `default_project_visibility` | integer | no | What visibility level new projects receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
+| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
+| `restricted_signup_domains` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
+| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
+| `after_sign_out_path` | string | no | Where to redirect users after logout |
-All parameters are optional. You can send only one that you want to change.
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
+```
+Example response:
```json
{
@@ -79,7 +89,7 @@ All parameters are optional. You can send only one that you want to change.
"restricted_visibility_levels": [],
"max_attachment_size": 10,
"session_expire_delay": 10080,
- "default_project_visibility": 0,
+ "default_project_visibility": 1,
"default_snippet_visibility": 0,
"restricted_signup_domains": [],
"user_oauth_applications": true,
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index f9637d8a6c4..dc036d7e27f 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -1,40 +1,71 @@
# System hooks
-All methods require admin authorization.
+All methods require administrator authorization.
-The URL endpoint of the system hooks can be configured in [the admin area under hooks](/admin/hooks).
+The URL endpoint of the system hooks can also be configured using the UI in
+the admin area under **Hooks** (`/admin/hooks`).
+
+Read more about [system hooks](../system_hooks/system_hooks.md).
## List system hooks
-Get list of system hooks
+Get a list of all system hooks.
+
+---
```
GET /hooks
```
-Parameters:
+Example request:
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks
+```
-- **none**
+Example response:
```json
[
- {
- "id": 3,
- "url": "http://example.com/hook",
- "created_at": "2013-10-02T10:15:31Z"
- }
+ {
+ "id" : 1,
+ "url" : "https://gitlab.example.com/hook",
+ "created_at" : "2015-11-04T20:07:35.874Z"
+ }
]
```
-## Add new system hook hook
+## Add new system hook
+
+Add a new system hook.
+
+---
```
POST /hooks
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `url` | string | yes | The hook URL |
+
+Example request:
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook"
+```
+
+Example response:
-- `url` (required) - The hook URL
+```json
+[
+ {
+ "id" : 2,
+ "url" : "https://gitlab.example.com/hook",
+ "created_at" : "2015-11-04T20:07:35.874Z"
+ }
+]
+```
## Test system hook
@@ -42,29 +73,68 @@ Parameters:
GET /hooks/:id
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the hook |
-- `id` (required) - The ID of hook
+Example request:
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
+```
+
+Example response:
```json
{
- "event_name": "project_create",
- "name": "Ruby",
- "path": "ruby",
- "project_id": 1,
- "owner_name": "Someone",
- "owner_email": "example@gitlabhq.com"
+ "project_id" : 1,
+ "owner_email" : "example@gitlabhq.com",
+ "owner_name" : "Someone",
+ "name" : "Ruby",
+ "path" : "ruby",
+ "event_name" : "project_create"
}
```
## Delete system hook
-Deletes a system hook. This is an idempotent API function and returns `200 OK` even if the hook is not available. If the hook is deleted it is also returned as JSON.
+Deletes a system hook. This is an idempotent API function and returns `200 OK`
+even if the hook is not available.
+
+If the hook is deleted, a JSON object is returned. An error is raised if the
+hook is not found.
+
+---
```
DELETE /hooks/:id
```
-Parameters:
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the hook |
+
+Example request:
-- `id` (required) - The ID of hook
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
+```
+
+Example response:
+
+```json
+{
+ "note_events" : false,
+ "project_id" : null,
+ "enable_ssl_verification" : true,
+ "url" : "https://gitlab.example.com/hook",
+ "updated_at" : "2015-11-04T20:12:15.931Z",
+ "issues_events" : false,
+ "merge_requests_events" : false,
+ "created_at" : "2015-11-04T20:12:15.931Z",
+ "service_id" : null,
+ "id" : 2,
+ "push_events" : true,
+ "tag_push_events" : false
+}
+```
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 085d387e824..17d12e9cc62 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -83,6 +83,26 @@ it will contain the annotation.
It returns 200 if the operation succeed. In case of an error,
405 with an explaining error message is returned.
+## Delete a tag
+
+Deletes a tag of a repository with given name. On success, this API method
+returns 200 with the name of the deleted tag. If the tag does not exist, the
+API returns 404.
+
+```
+DELETE /projects/:id/repository/tags/:tag_name
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+
+```json
+{
+ "tag_name": "v4.3.0"
+}
+```
## Create a new release
diff --git a/doc/api/users.md b/doc/api/users.md
index 66d2fd52526..383e7c76ab0 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -123,6 +123,13 @@ Parameters:
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
+ "created_at": "2012-05-23T08:00:58Z",
+ "is_admin": false,
+ "bio": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": ""
}
```
@@ -144,6 +151,8 @@ Parameters:
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z",
+ "confirmed_at": "2012-05-23T08:00:58Z",
+ "last_sign_in_at": "2015-03-23T08:00:58Z",
"bio": null,
"skype": "",
"linkedin": "",
@@ -185,6 +194,7 @@ Parameters:
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false
+- `external` (optional) - Flags the user as external - true or false(default)
## User modification
@@ -210,6 +220,7 @@ Parameters:
- `bio` - User's biography
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
+- `external` (optional) - Flags the user as external - true or false(default)
Note, at the moment this method does only return a 404 error,
even in cases where a 409 (Conflict) would be more appropriate,
@@ -551,7 +562,8 @@ Parameters:
- `uid` (required) - id of specified user
-Will return `200 OK` on success, or `404 User Not Found` is user cannot be found.
+Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+`403 Forbidden` when trying to block an already blocked user by LDAP synchronization.
## Unblock user
@@ -565,4 +577,5 @@ Parameters:
- `uid` (required) - id of specified user
-Will return `200 OK` on success, or `404 User Not Found` is user cannot be found.
+Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index a1f5513d88e..4abc45bf9bb 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,37 +1,18 @@
## GitLab CI Documentation
-### User documentation
-
-* [Quick Start](quick_start/README.md)
-* [Configuring project (.gitlab-ci.yml)](yaml/README.md)
-* [Configuring runner](runners/README.md)
-* [Configuring deployment](deployment/README.md)
-* [Using Docker Images](docker/using_docker_images.md)
-* [Using Docker Build](docker/using_docker_build.md)
-* [Using Variables](variables/README.md)
-* [Using SSH keys](ssh_keys/README.md)
-* [Triggering builds through the API](triggers/README.md)
-
-### Languages
-
-* [Testing PHP](languages/php.md)
-
-### Services
-
-* [Using MySQL](services/mysql.md)
-* [Using PostgreSQL](services/postgres.md)
-* [Using Redis](services/redis.md)
-* [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
-
-### Examples
-
-+ [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-+ [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
-+ [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
-+ [Test Clojure applications](examples/test-clojure-application.md)
-+ Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
-
-### Administrator documentation
-
-* [User permissions](permissions/README.md)
-* [API](api/README.md)
+### CI User documentation
+
+- [Get started with GitLab CI](quick_start/README.md)
+- [CI examples for various languages](examples/README.md)
+- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
+- [Learn how `.gitlab-ci.yml` works](yaml/README.md)
+- [Configure a Runner, the application that runs your builds](runners/README.md)
+- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
+- [Use CI to build Docker images](docker/using_docker_build.md)
+- [Use variables in your `.gitlab-ci.yml`](variables/README.md)
+- [Use SSH keys in your build environment](ssh_keys/README.md)
+- [Trigger builds through the API](triggers/README.md)
+- [Build artifacts](build_artifacts/README.md)
+- [User permissions](permissions/README.md)
+- [API](api/README.md)
+- [CI services (linked docker containers)](services/README.md)
diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md
index cf9710ede57..aea808007fc 100644
--- a/doc/ci/api/README.md
+++ b/doc/ci/api/README.md
@@ -1,86 +1,22 @@
# GitLab CI API
-## Resources
-
-- [Projects](projects.md)
-- [Runners](runners.md)
-- [Commits](commits.md)
-- [Builds](builds.md)
-
-
-## Authentication
-
-GitLab CI API uses different types of authentication depends on what API you use.
-Each API document has section with information about authentication you need to use.
-
-GitLab CI API has 4 authentication methods:
-
-* GitLab user token & GitLab url
-* GitLab CI project token
-* GitLab CI runners registration token
-* GitLab CI runner token
-
-
-### Authentication #1: GitLab user token & GitLab url
-
-Authentication is done by
-sending the `private-token` of a valid user and the `url` of an
-authorized GitLab instance via a query string along with the API
-request:
-
- GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/
+## Purpose
-If preferred, you may instead send the `private-token` as a header in
-your request:
+Main purpose of GitLab CI API is to provide necessary data and context for
+GitLab CI Runners.
- curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/"
+For consumer API take a look at this [documentation](../../api/README.md) where
+you will find all relevant information.
+## API Prefix
-### Authentication #2: GitLab CI project token
+Current CI API prefix is `/ci/api/v1`.
-Each project in GitLab CI has it own token.
-It can be used to get project commits and builds information.
-You can use project token only for certain project.
+You need to prepend this prefix to all examples in this documentation, like:
-### Authentication #3: GitLab CI runners registration token
+ GET /ci/api/v1/builds/:id/artifacts
-This token is not persisted and is generated on each application start.
-It can be used only for registering new runners in system. You can find it on
-GitLab CI Runners web page https://gitlab-ci.example.com/admin/runners
-
-### Authentication #4: GitLab CI runner token
-
-Every GitLab CI runner has it own token that allow it to receive and update
-GitLab CI builds. This token exists of internal purposes and should be used only
-by runners
-
-## JSON
-
-All API requests are serialized using JSON. You don't need to specify
-`.json` at the end of API URL.
-
-## Status codes
-
-The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave.
-
-API request types:
-
-- `GET` requests access one or more resources and return the result as JSON
-- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON
-- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON
-- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not.
-
-The following list shows the possible return codes for API requests.
-
-Return values:
+## Resources
-- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON
-- `201 Created` - The `POST` request was successful and the resource is returned as JSON
-- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given
-- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above
-- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project
-- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found
-- `405 Method Not Allowed` - The request is not supported
-- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists
-- `422 Unprocessable` - The entity could not be processed
-- `500 Server Error` - While handling the request something went wrong on the server side
+- [Builds](builds.md)
+- [Runners](runners.md)
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
index 3b5008ccdb4..d100e261178 100644
--- a/doc/ci/api/builds.md
+++ b/doc/ci/api/builds.md
@@ -1,41 +1,73 @@
# Builds API
-This API used by runners to receive and update builds.
+API used by runners to receive and update builds.
-__Authentication is done by runner token__
+_**Note:** This API is intended to be used only by Runners as their own
+communication channel. For the consumer API see the
+[Builds API](../../api/builds.md)._
+
+## Authentication
+
+This API uses two types of authentication:
+
+1. Unique runner's token
+
+ Token assigned to runner after it has been registered.
+
+2. Using build authorization token
+
+ This is project's CI token that can be found in Continuous Integration
+ project settings.
+
+ Build authorization token can be passed as a parameter or a value of
+ `BUILD-TOKEN` header. This method are interchangeable.
## Builds
### Runs oldest pending build by runner
- POST /ci/builds/register
+ POST /ci/api/v1/builds/register
Parameters:
- * `token` (required) - The unique token of runner
-
-Returns:
-
-```json
-{
- "id" : 79,
- "commands" : "",
- "path" : "",
- "ref" : "",
- "sha" : "",
- "project_id" : 6,
- "repo_url" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
- "before_sha" : ""
-}
-```
+ * `token` (required) - Unique runner token
### Update details of an existing build
- PUT /ci/builds/:id
+ PUT /ci/api/v1/builds/:id
Parameters:
* `id` (required) - The ID of a project
+ * `token` (required) - Unique runner token
* `state` (optional) - The state of a build
* `trace` (optional) - The trace of a build
+
+### Upload artifacts to build
+
+ POST /ci/api/v1/builds/:id/artifacts
+
+Parameters:
+
+ * `id` (required) - The ID of a build
+ * `token` (required) - The build authorization token
+ * `file` (required) - Artifacts file
+
+### Download the artifacts file from build
+
+ GET /ci/api/v1/builds/:id/artifacts
+
+Parameters:
+
+ * `id` (required) - The ID of a build
+ * `token` (required) - The build authorization token
+
+### Remove the artifacts file from build
+
+ DELETE /ci/api/v1/builds/:id/artifacts
+
+Parameters:
+
+ * ` id` (required) - The ID of a build
+ * `token` (required) - The build authorization token
diff --git a/doc/ci/api/commits.md b/doc/ci/api/commits.md
deleted file mode 100644
index 4df7afc6c52..00000000000
--- a/doc/ci/api/commits.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# Commits API
-
-__Authentication is done by GitLab CI project token__
-
-## Commits
-
-### Retrieve all commits per project
-
-Get list of commits per project
-
- GET /ci/commits
-
-Parameters:
-
- * `project_id` (required) - The ID of a project
- * `project_token` (requires) - Project token
- * `page` (optional)
- * `per_page` (optional) - items per request (default is 20)
-
-Returns:
-
-```json
-[{
- "id": 3,
- "ref": "master",
- "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
- "project_id": 2,
- "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
- "created_at": "2014-11-05T09:46:35.247Z",
- "status": "success",
- "finished_at": "2014-11-05T09:46:44.254Z",
- "duration": 5.062692165374756,
- "git_commit_message": "wow\n",
- "git_author_name": "Administrator",
- "git_author_email": "admin@example.com",
- "builds": [{
- "id": 7,
- "project_id": 2,
- "ref": "master",
- "status": "success",
- "finished_at": "2014-11-05T09:46:44.254Z",
- "created_at": "2014-11-05T09:46:35.259Z",
- "updated_at": "2014-11-05T09:46:44.255Z",
- "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
- "started_at": "2014-11-05T09:46:39.192Z",
- "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
- "runner_id": 1,
- "coverage": null,
- "commit_id": 3
- }]
-}]
-```
-
-### Create commit
-
-Inform GitLab CI about new commit you want it to build.
-
-__If commit already exists in GitLab CI it will not be created__
-
-
- POST /ci/commits
-
-Parameters:
-
- * `project_id` (required) - The ID of a project
- * `project_token` (requires) - Project token
- * `data` (required) - Push data. For example see comment in `lib/api/commits.rb`
-
-Returns:
-
-```json
-{
- "id": 3,
- "ref": "master",
- "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
- "project_id": 2,
- "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
- "created_at": "2014-11-05T09:46:35.247Z",
- "status": "success",
- "finished_at": "2014-11-05T09:46:44.254Z",
- "duration": 5.062692165374756,
- "git_commit_message": "wow\n",
- "git_author_name": "Administrator",
- "git_author_email": "admin@example.com",
- "builds": [{
- "id": 7,
- "project_id": 2,
- "ref": "master",
- "status": "success",
- "finished_at": "2014-11-05T09:46:44.254Z",
- "created_at": "2014-11-05T09:46:35.259Z",
- "updated_at": "2014-11-05T09:46:44.255Z",
- "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
- "started_at": "2014-11-05T09:46:39.192Z",
- "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
- "runner_id": 1,
- "coverage": null,
- "commit_id": 3
- }]
-}
-```
diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md
deleted file mode 100644
index 74a4c64d000..00000000000
--- a/doc/ci/api/projects.md
+++ /dev/null
@@ -1,149 +0,0 @@
-# Projects API
-
-This API is intended to aid in the setup and configuration of
-projects on GitLab CI.
-
-__Authentication is done by GitLab user token & GitLab url__
-
-## Projects
-
-### List Authorized Projects
-
-Lists all projects that the authenticated user has access to.
-
-```
-GET /ci/projects
-```
-
-Returns:
-
-```json
- [
- {
- "id" : 271,
- "name" : "gitlabhq",
- "timeout" : 1800,
- "token" : "iPWx6WM4lhHNedGfBpPJNP",
- "default_ref" : "master",
- "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
- "path" : "gitlab/gitlab-shell",
- "always_build" : false,
- "polling_interval" : null,
- "public" : false,
- "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
- "gitlab_id" : 3
- },
- {
- "id" : 272,
- "name" : "gitlab-ci",
- "timeout" : 1800,
- "token" : "iPWx6WM4lhHNedGfBpPJNP",
- "default_ref" : "master",
- "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
- "path" : "gitlab/gitlab-shell",
- "always_build" : false,
- "polling_interval" : null,
- "public" : false,
- "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
- "gitlab_id" : 4
- }
-]
-```
-
-### List Owned Projects
-
-Lists all projects that the authenticated user owns.
-
-```
-GET /ci/projects/owned
-```
-
-Returns:
-
-```json
-[
- {
- "id" : 272,
- "name" : "gitlab-ci",
- "timeout" : 1800,
- "token" : "iPWx6WM4lhHNedGfBpPJNP",
- "default_ref" : "master",
- "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
- "path" : "gitlab/gitlab-shell",
- "always_build" : false,
- "polling_interval" : null,
- "public" : false,
- "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
- "gitlab_id" : 4
- }
-]
-```
-
-### Single Project
-
-Returns information about a single project for which the user is
-authorized.
-
- GET /ci/projects/:id
-
-Parameters:
-
- * `id` (required) - The ID of the GitLab CI project
-
-### Create Project
-
-Creates a GitLab CI project using GitLab project details.
-
- POST /ci/projects
-
-Parameters:
-
- * `name` (required) - The name of the project
- * `gitlab_id` (required) - The ID of the project on the GitLab instance
- * `default_ref` (optional) - The branch to run on (default to `master`)
-
-### Update Project
-
-Updates a GitLab CI project using GitLab project details that the
-authenticated user has access to.
-
- PUT /ci/projects/:id
-
-Parameters:
-
- * `name` - The name of the project
- * `default_ref` - The branch to run on (default to `master`)
-
-### Remove Project
-
-Removes a GitLab CI project that the authenticated user has access to.
-
- DELETE /ci/projects/:id
-
-Parameters:
-
- * `id` (required) - The ID of the GitLab CI project
-
-### Link Project to Runner
-
-Links a runner to a project so that it can make builds (only via
-authorized user).
-
- POST /ci/projects/:id/runners/:runner_id
-
-Parameters:
-
- * `id` (required) - The ID of the GitLab CI project
- * `runner_id` (required) - The ID of the GitLab CI runner
-
-### Remove Project from Runner
-
-Removes a runner from a project so that it can not make builds (only
-via authorized user).
-
- DELETE /ci/projects/:id/runners/:runner_id
-
-Parameters:
-
- * `id` (required) - The ID of the GitLab CI project
- * `runner_id` (required) - The ID of the GitLab CI runner \ No newline at end of file
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
index c383dc4bcc9..2f01da4bd76 100644
--- a/doc/ci/api/runners.md
+++ b/doc/ci/api/runners.md
@@ -1,77 +1,46 @@
# Runners API
-## Runners
+API used by runners to register and delete themselves.
-### Retrieve all runners
+_**Note:** This API is intended to be used only by Runners as their own
+communication channel. For the consumer API see the
+[new Runners API](../../api/runners.md)._
-__Authentication is done by GitLab user token & GitLab url__
+## Authentication
-Used to get information about all runners registered on the GitLab CI
-instance.
+This API uses two types of authentication:
- GET /ci/runners
+1. Unique runner's token
-Returns:
+ Token assigned to runner after it has been registered.
-```json
-[
- {
- "id" : 85,
- "token" : "12b68e90394084703135"
- },
- {
- "id" : 86,
- "token" : "76bf894e969364709864"
- },
-]
-```
+2. Using runners' registration token
-### Register a new runner
+ This is a token that can be found in project's settings.
+ It can be also found in Admin area &raquo; Runners settings.
+
+ There are two types of tokens you can pass - shared runner registration
+ token or project specific registration token.
+## Runners
-__Authentication is done with a Shared runner registration token or a project Specific runner registration token__
+### Register a new runner
Used to make GitLab CI aware of available runners.
- POST /ci/runners/register
+ POST /ci/api/v1/runners/register
Parameters:
- * `token` (required) - The registration token. It is 2 types of token you can pass here.
-
-1. Shared runner registration token
-2. Project specific registration token
+ * `token` (required) - Registration token
-Returns:
-
-```json
-{
- "id" : 85,
- "token" : "12b68e90394084703135"
-}
-```
### Delete a runner
+Used to remove runner.
-__Authentication is done by runner token__
-
-Used to removing runners.
-
- DELETE /ci/runners/delete
+ DELETE /ci/api/v1/runners/delete
Parameters:
- * `token` (required) - The runner token.
-
-Returns:
-
-```json
-{
- "id" : 1,
- "token" : "d14963981a428f70121777e50643d1",
- "created_at" : "2015-02-26T11:39:39.232Z",
- "updated_at" : "2015-02-26T11:39:39.232Z",
- "description" : "awesome runner"
-}
-``` \ No newline at end of file
+ * `token` (required) - Unique runner token
diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md
new file mode 100644
index 00000000000..71db5aa5dc8
--- /dev/null
+++ b/doc/ci/build_artifacts/README.md
@@ -0,0 +1,176 @@
+# Introduction to build artifacts
+
+Artifacts is a list of files and directories which are attached to a build
+after it completes successfully.
+
+Since GitLab 8.2 and [GitLab Runner] 0.7.0, build artifacts that are created by
+GitLab Runner are uploaded to GitLab and are downloadable as a single archive
+(`tar.gz`) using the GitLab UI.
+
+Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
+changed to `ZIP`, and it is now possible to browse its contents, with the added
+ability of downloading the files separately.
+
+**Note:**
+The artifacts browser will be available only for new artifacts that are sent
+to GitLab using GitLab Runner version 1.0 and up. It will not be possible to
+browse old artifacts already uploaded to GitLab.
+
+## Enabling build artifacts
+
+_If you are searching for ways to use artifacts, jump to
+[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._
+
+The artifacts feature is enabled by default in all GitLab installations.
+To disable it site-wide, follow the steps below.
+
+---
+
+**In Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['artifacts_enabled'] = false
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ artifacts:
+ enabled: false
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Defining artifacts in `.gitlab-ci.yml`
+
+A simple example of using the artifacts definition in `.gitlab-ci.yml` would be
+the following:
+
+```yaml
+pdf:
+ script: xelatex mycv.tex
+ artifacts:
+ paths:
+ - mycv.pdf
+```
+
+A job named `pdf` calls the `xelatex` command in order to build a pdf file from
+the latex source file `mycv.tex`. We then define the `artifacts` paths which in
+turn are defined with the `paths` keyword. All paths to files and directories
+are relative to the repository that was cloned during the build.
+
+For more examples on artifacts, follow the
+[separate artifacts yaml documentation](../yaml/README.md#artifacts).
+
+## Storing build artifacts
+
+After a successful build, GitLab Runner uploads an archive containing the build
+artifacts to GitLab.
+
+To change the location where the artifacts are stored, follow the steps below.
+
+---
+
+**In Omnibus installations:**
+
+_The artifacts are stored by default in
+`/var/opt/gitlab/gitlab-rails/shared/artifacts`._
+
+1. To change the storage path for example to `/mnt/storage/artifacts`, edit
+ `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['artifacts_path'] = "/mnt/storage/artifacts"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+_The artifacts are stored by default in
+`/home/git/gitlab/shared/artifacts`._
+
+1. To change the storage path for example to `/mnt/storage/artifacts`, edit
+ `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ artifacts:
+ enabled: true
+ path: /mnt/storage/artifacts
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Browsing build artifacts
+
+When GitLab receives an artifacts archive, an archive metadata file is also
+generated. This metadata file describes all the entries that are located in the
+artifacts archive itself. The metadata file is in a binary format, with
+additional GZIP compression.
+
+GitLab does not extract the artifacts archive in order to save space, memory
+and disk I/O. It instead inspects the metadata file which contains all the
+relevant information. This is especially important when there is a lot of
+artifacts, or an archive is a very large file.
+
+---
+
+After a successful build, if you visit the build's specific page, you can see
+that there are two buttons.
+
+One is for downloading the artifacts archive and the other for browsing its
+contents.
+
+![Build artifacts browser button](img/build_artifacts_browser_button.png)
+
+---
+
+The archive browser shows the name and the actual file size of each file in the
+archive. If your artifacts contained directories, then you are also able to
+browse inside them.
+
+Below you can see an image of three different file formats, as well as two
+directories.
+
+![Build artifacts browser](img/build_artifacts_browser.png)
+
+---
+
+## Downloading build artifacts
+
+If you need to download the whole archive, there are buttons in various places
+inside GitLab that make that possible.
+
+1. While on the builds page, you can see the download icon for each build's
+ artifacts archive in the right corner
+
+1. While inside a specific build, you are presented with a download button
+ along with the one that browses the archive
+
+1. And finally, when browsing an archive you can see the download button at
+ the top right corner
+
+---
+
+Note that GitLab does not extract the entire artifacts archive to send just a
+single file to the user.
+
+When clicking on a specific file, [GitLab Workhorse] extracts it from the
+archive and the download begins.
+
+This implementation saves space, memory and disk I/O.
+
+[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner "GitLab Runner repository"
+[reconfigure gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation"
+[restart gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation"
+[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser.png b/doc/ci/build_artifacts/img/build_artifacts_browser.png
new file mode 100644
index 00000000000..73ed4eeb927
--- /dev/null
+++ b/doc/ci/build_artifacts/img/build_artifacts_browser.png
Binary files differ
diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser_button.png b/doc/ci/build_artifacts/img/build_artifacts_browser_button.png
new file mode 100644
index 00000000000..f5d15bc3e7d
--- /dev/null
+++ b/doc/ci/build_artifacts/img/build_artifacts_browser_button.png
Binary files differ
diff --git a/doc/ci/deployment/README.md b/doc/ci/deployment/README.md
index ffd841ca9e7..7d91ce6710f 100644
--- a/doc/ci/deployment/README.md
+++ b/doc/ci/deployment/README.md
@@ -89,7 +89,7 @@ We also use two secure variables:
In GitLab CI 7.12 a new feature was introduced: Secure Variables.
Secure Variables can added by going to `Project > Variables > Add Variable`.
**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.**
-The variables that are defined in the project settings are send along with the build script to the runner.
+The variables that are defined in the project settings are sent along with the build script to the runner.
The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml.
It is also important that secret's value is hidden in the build log.
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 31458d61674..bd748f1b986 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -174,7 +174,7 @@ The alias hostname for the service is made from the image name following these
rules:
1. Everything after `:` is stripped
-2. Backslash (`/`) is replaced with double underscores (`__`)
+2. Slash (`/`) is replaced with double underscores (`__`)
## Configuring services
@@ -270,7 +270,7 @@ This will forcefully (`-f`) remove the `build` container, the two service
containers as well as all volumes (`-v`) that were created with the container
creation.
-[Docker Fundamentals]: https://docs.docker.com/engine/introduction/understanding-docker/
+[Docker Fundamentals]: https://docs.docker.com/engine/understanding-docker/
[hub]: https://hub.docker.com/
[linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/
[tutum/wordpress]: https://registry.hub.docker.com/u/tutum/wordpress/
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
new file mode 100644
index 00000000000..c10f82054e2
--- /dev/null
+++ b/doc/ci/enable_or_disable_ci.md
@@ -0,0 +1,70 @@
+## Enable or disable GitLab CI
+
+_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
+file present at the root directory of your project and a
+[runner](runners/README.md) properly set up. You can read our
+[quick start guide](quick_start/README.md) to get you started._
+
+If you are using an external CI server like Jenkins or Drone CI, it is advised
+to disable GitLab CI in order to not have any conflicts with the commits status
+API.
+
+---
+
+As of GitLab 8.2, GitLab CI is mainly exposed via the `/builds` page of a
+project. Disabling GitLab CI in a project does not delete any previous builds.
+In fact, the `/builds` page can still be accessed, although it's hidden from
+the left sidebar menu.
+
+GitLab CI is enabled by default on new installations and can be disabled either
+individually under each project's settings, or site-wide by modifying the
+settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
+respectively.
+
+### Per-project user setting
+
+The setting to enable or disable GitLab CI can be found with the name **Builds**
+under the **Features** area of a project's settings along with **Issues**,
+**Merge Requests**, **Wiki** and **Snippets**. Select or deselect the checkbox
+and hit **Save** for the settings to take effect.
+
+![Features settings](img/features_settings.png)
+
+---
+
+### Site-wide administrator setting
+
+You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml`
+and `gitlab.rb` for source and Omnibus installations respectively.
+
+Two things to note:
+
+1. Disabling GitLab CI, will affect only newly-created projects. Projects that
+ had it enabled prior to this modification, will work as before.
+1. Even if you disable GitLab CI, users will still be able to enable it in the
+ project's settings.
+
+---
+
+For installations from source, open `gitlab.yml` with your editor and set
+`builds` to `false`:
+
+```yaml
+## Default project features settings
+default_projects_features:
+ issues: true
+ merge_requests: true
+ wiki: true
+ snippets: false
+ builds: false
+```
+
+Save the file and restart GitLab: `sudo service gitlab restart`.
+
+For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line:
+
+```
+gitlab_rails['gitlab_default_projects_features_builds'] = false
+```
+
+Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 1cf41aea391..cc059dc4376 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,5 +1,15 @@
-# Build script examples
+# CI Examples
-+ [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
-+ [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
-+ [Test a Clojure application](test-clojure-application.md)
+- [Testing a PHP application](php.md)
+- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
+- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
+- [Test a Clojure application](test-clojure-application.md)
+- [Using `dpl` as deployment tool](deployment/README.md)
+- Help your favorite programming language and GitLab by sending a merge request
+ with a guide for that language.
+
+## Outside the documentation
+
+- [Blost post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
+- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
diff --git a/doc/ci/languages/php.md b/doc/ci/examples/php.md
index dacb67fa3ff..aeadd6a448e 100644
--- a/doc/ci/languages/php.md
+++ b/doc/ci/examples/php.md
@@ -12,7 +12,7 @@ configuration from the developer. To overcome this we will be using the
official [PHP docker image][php-hub] that can be found in Docker Hub.
This will allow us to test PHP projects against different versions of PHP.
-However, not everything is plug 'n' play, you still need to onfigure some
+However, not everything is plug 'n' play, you still need to configure some
things manually.
As with every build, you need to create a valid `.gitlab-ci.yml` describing the
@@ -97,7 +97,7 @@ image: php:5.6
before_script:
# Install dependencies
-- ci/docker_install.sh > /dev/null
+- bash ci/docker_install.sh > /dev/null
test:app:
script:
@@ -112,7 +112,7 @@ with a different docker image version and the runner will do the rest:
```yaml
before_script:
# Install dependencies
-- ci/docker_install.sh > /dev/null
+- bash ci/docker_install.sh > /dev/null
# We test PHP5.6
test:5.6:
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index e52e1547461..f5645d586ae 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -1,5 +1,5 @@
## Test and Deploy a ruby application
-This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application.
+This example will guide you how to run tests in your Ruby application and deploy it automatically as Heroku application.
You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all).
@@ -56,12 +56,12 @@ gitlab-ci-multi-runner register \
--non-interactive \
--url "https://gitlab.com/ci/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
- --description "ruby-2.1" \
+ --description "ruby-2.2" \
--executor "docker" \
- --docker-image ruby:2.1 \
+ --docker-image ruby:2.2 \
--docker-postgres latest
```
-With the command above, you create a runner that uses [ruby:2.1](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database.
+With the command above, you create a runner that uses [ruby:2.2](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database.
To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
diff --git a/doc/ci/img/features_settings.png b/doc/ci/img/features_settings.png
new file mode 100644
index 00000000000..17aba5d14d8
--- /dev/null
+++ b/doc/ci/img/features_settings.png
Binary files differ
diff --git a/doc/ci/languages/README.md b/doc/ci/languages/README.md
deleted file mode 100644
index 54b2343e08b..00000000000
--- a/doc/ci/languages/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-### Languages
-
-This is a list of languages you can test with GitLab CI. Each section has
-comprehensive documentation and comes with a test repository hosted on
-GitLab.com
-
-+ [Testing PHP](php.md)
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index a9b36139de9..9aba4326e11 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,25 +1,47 @@
# Quick Start
-Starting from version 8.0, GitLab Continuous Integration (CI) is fully
-integrated into GitLab itself and is enabled by default on all projects.
+>**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
+is fully integrated into GitLab itself and is [enabled] by default on all
+projects.
-This guide assumes that you:
+The TL;DR version of how GitLab CI works is the following.
-- have a working GitLab instance of version 8.0 or higher or are using
- [GitLab.com](https://gitlab.com/users/sign_in)
-- have a project in GitLab that you would like to use CI for
+---
+
+GitLab offers a [continuous integration][ci] service. If you
+[add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
+and configure your GitLab project to use a [Runner], then each merge request or
+push triggers a build.
+
+The `.gitlab-ci.yml` file tells the GitLab runner what do to. By default it
+runs three [stages]: `build`, `test`, and `deploy`.
-In brief, the steps needed to have a working CI can be summed up to:
+If everything runs OK (no non-zero return values), you'll get a nice green
+checkmark associated with the pushed commit or merge request. This makes it
+easy to see whether a merge request will cause any of the tests to fail before
+you even look at the code.
-1. Create a new project
-1. Add `.gitlab-ci.yml` to the git repository and push to GitLab
+Most projects only use GitLab's CI service to run the test suite so that
+developers get immediate feedback if they broke something.
+
+So in brief, the steps needed to have a working CI can be summed up to:
+
+1. Add `.gitlab-ci.yml` to the root directory of your repository
1. Configure a Runner
-From there on, on every push to your git repository the build will be
+From there on, on every push to your Git repository, the build will be
automagically started by the Runner and will appear under the project's
`/builds` page.
-Now, let's break it down to pieces and work on solving the GitLab CI puzzle.
+---
+
+This guide assumes that you:
+
+- have a working GitLab instance of version 8.0 or higher or are using
+ [GitLab.com](https://gitlab.com/users/sign_in)
+- have a project in GitLab that you would like to use CI for
+
+Let's break it down to pieces and work on solving the GitLab CI puzzle.
## Creating a `.gitlab-ci.yml` file
@@ -36,13 +58,13 @@ file and start builds on _Runners_ according to the contents of the file,
for that commit.
Because `.gitlab-ci.yml` is in the repository, it is version controlled,
-old versions still build succesfully, forks can easily make use of CI,
+old versions still build successfully, forks can easily make use of CI,
branches can have separate builds and you have a single source of truth for CI.
You can read more about the reasons why we are using `.gitlab-ci.yml`
[in our blog about it][blog-ci].
**Note:** `.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file
-so you have to pay extra attention to the identation. Always use spaces, not
+so you have to pay extra attention to the indentation. Always use spaces, not
tabs.
### Creating a simple `.gitlab-ci.yml` file
@@ -124,7 +146,7 @@ In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`.
A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker
container or even a cluster of containers. GitLab and the Runners communicate
through an API, so the only needed requirement is that the machine on which the
-Runner is configured to has Internet access.
+Runner is configured to have Internet access.
A Runner can be specific to a certain project or serve multiple projects in
GitLab. If it serves all projects it's called a _Shared Runner_.
@@ -168,7 +190,7 @@ To enable **Shared Runners** you have to go to your project's
## Seeing the status of your build
-After configuring the Runner succesfully, you should see the status of your
+After configuring the Runner successfully, you should see the status of your
last commit change from _pending_ to either _running_, _success_ or _failed_.
You can view all builds, by going to the **Builds** page in your project.
@@ -184,14 +206,35 @@ you expected.
You are also able to view the status of any commit in the various pages in
GitLab, such as **Commits** and **Merge Requests**.
-## Next steps
+## Enabling build emails
+
+If you want to receive e-mail notifications about the result status of the
+builds, you should explicitly enable the **Builds Emails** service under your
+project's settings.
+
+For more information read the [Builds emails service documentation]
+(../../project_services/builds_emails.md).
+
+## Builds badge
+
+You can access a builds badge image using following link:
+
+```
+http://example.gitlab.com/namespace/project/badges/branch/build.svg
+```
Awesome! You started using CI in GitLab!
-Next you can look into doing more with the CI. Many people are using GitLab
-to package, containerize, test and deploy software.
+## Examples
-Visit our various languages examples at <https://gitlab.com/groups/gitlab-examples>.
+Visit the [examples README][examples] to see a list of examples using GitLab
+CI with various languages.
[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
+[examples]: ../examples/README.md
+[ci]: https://about.gitlab.com/gitlab-ci/
+[yaml]: ../yaml/README.md
+[runner]: ../runners/README.md
+[enabled]: ../enable_or_disable_ci.md
+[stages]: ../yaml/README.md#stages
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 68dcfe23ffb..295d953db11 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -62,8 +62,9 @@ Now simply register the runner as any runner:
sudo gitlab-runner register
```
-Note that you will have to enable `Allows shared runners` for each project
-that you want to make use of a shared runner. This is by default `off`.
+Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the
+`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared runners to
+disabled.
## Registering a Specific Runner
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
index 1ebb0a4a250..4b79461d55c 100644
--- a/doc/ci/services/README.md
+++ b/doc/ci/services/README.md
@@ -1,9 +1,9 @@
## GitLab CI Services
-GitLab CI uses the `services` keyword to define what docker containers should be
-linked with your base image. Below is a list of examples you may use.
+GitLab CI uses the `services` keyword to define what docker containers should
+be linked with your base image. Below is a list of examples you may use.
-+ [Using MySQL](mysql.md)
-+ [Using PostgreSQL](postgres.md)
-+ [Using Redis](redis.md)
-+ [Using Other Services](../docker/using_docker_images.md#how-to-use-other-images-as-services)
+- [Using MySQL](mysql.md)
+- [Using PostgreSQL](postgres.md)
+- [Using Redis](redis.md)
+- [Using Other Services](../docker/using_docker_images.md#how-to-use-other-images-as-services)
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index b99ea25a3fe..b0e53cbc261 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -16,7 +16,7 @@ The API_TOKEN will take the Secure Variable value: `SECURE`.
### Predefined variables (Environment Variables)
| Variable | Runner | Description |
-|-------------------------|-------------|
+|-------------------------|-----|--------|
| **CI** | 0.4 | Mark that build is executed in CI environment |
| **GITLAB_CI** | all | Mark that build is executed in GitLab CI environment |
| **CI_SERVER** | all | Mark that build is executed in CI environment |
@@ -30,7 +30,7 @@ The API_TOKEN will take the Secure Variable value: `SECURE`.
| **CI_BUILD_REF_NAME** | all | The branch or tag name for which project is built |
| **CI_BUILD_ID** | all | The unique id of the current build that GitLab CI uses internally |
| **CI_BUILD_REPO** | all | The URL to clone the Git repository |
-| **CI_BUILD_TRIGGERED** | 0.5 | The flag to indicate that build was triggered |
+| **CI_BUILD_TRIGGERED** | 0.5 | The flag to indicate that build was [triggered] |
| **CI_PROJECT_ID** | all | The unique id of the current project that GitLab CI uses internally |
| **CI_PROJECT_DIR** | all | The full path where the repository is cloned and where the build is ran |
@@ -56,7 +56,7 @@ export CI_SERVER_VERSION=""
```
### YAML-defined variables
-**This feature requires GitLab Runner 0.5.0 or higher**
+**This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher **
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment.
The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL.
@@ -77,9 +77,12 @@ More information about Docker integration can be found in [Using Docker Images](
GitLab CI allows you to define per-project **Secure Variables** that are set in build environment.
The secure variables are stored out of the repository (the `.gitlab-ci.yml`).
-These variables are securely stored in GitLab CI database and are hidden in the build log.
+The variables are securely passed to GitLab Runner and are available in build environment.
It's desired method to use them for storing passwords, secret keys or whatever you want.
+**The value of the variable can be shown in build log if explicitly asked to do so.**
+If your project is public or internal you can make the builds private.
+
Secure Variables can added by going to `Project > Variables > Add Variable`.
They will be available for all subsequent builds.
@@ -101,3 +104,5 @@ job_name:
script:
- export
```
+
+[triggered]: ../triggers/README.md
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index fd0d49de4e4..a9b79bbdb1b 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -116,7 +116,8 @@ Alias for [stages](#stages).
### variables
-_**Note:** Introduced in GitLab Runner v0.5.0._
+>**Note:**
+Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build
environment. The variables are stored in the git repository and are meant to
@@ -134,22 +135,123 @@ thus allowing to fine tune them.
### cache
+>**Note:**
+Introduced in GitLab Runner v0.7.0.
+
`cache` is used to specify a list of files and directories which should be
-cached between builds. Caches are stored according to the branch/ref and the
-job name. They are not currently shared between different job names or between
-branches/refs, which means that caching will benefit you if you push subsequent
-commits to an existing feature branch.
+cached between builds.
+
+**By default the caching is enabled per-job and per-branch.**
If `cache` is defined outside the scope of the jobs, it means it is set
globally and all jobs will use its definition.
-To cache all git untracked files and files in `binaries`:
+Cache all files in `binaries` and `.config`:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ paths:
+ - binaries/
+ - .config
+```
+
+Cache all Git untracked files:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ untracked: true
+```
+
+Cache all Git untracked files and files in `binaries`:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ untracked: true
+ paths:
+ - binaries/
+```
+
+Locally defined cache overwrites globally defined options. This will cache only
+`binaries/`:
```yaml
cache:
- untracked: true
paths:
- - binaries/
+ - my/files
+
+rspec:
+ script: test
+ cache:
+ paths:
+ - binaries/
+```
+
+The cache is provided on best effort basis, so don't expect that cache will be
+always present. For implementation details please check GitLab Runner.
+
+#### cache:key
+
+>**Note:**
+Introduced in GitLab Runner v1.0.0.
+
+The `key` directive allows you to define the affinity of caching
+between jobs, allowing to have a single cache for all jobs,
+cache per-job, cache per-branch or any other way you deem proper.
+
+This allows you to fine tune caching, allowing you to cache data between
+different jobs or even different branches.
+
+The `cache:key` variable can use any of the [predefined variables](../variables/README.md).
+
+---
+
+**Example configurations**
+
+To enable per-job caching:
+
+```yaml
+cache:
+ key: "$CI_BUILD_NAME"
+ untracked: true
+```
+
+To enable per-branch caching:
+
+```yaml
+cache:
+ key: "$CI_BUILD_REF_NAME"
+ untracked: true
+```
+
+To enable per-job and per-branch caching:
+
+```yaml
+cache:
+ key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME"
+ untracked: true
+```
+
+To enable per-branch and per-stage caching:
+
+```yaml
+cache:
+ key: "$CI_BUILD_STAGE/$CI_BUILD_REF_NAME"
+ untracked: true
+```
+
+If you use **Windows Batch** to run your shell scripts you need to replace
+`$` with `%`:
+
+```yaml
+cache:
+ key: "%CI_BUILD_STAGE%/%CI_BUILD_REF_NAME%"
+ untracked: true
```
## Jobs
@@ -177,13 +279,14 @@ job_name:
| Keyword | Required | Description |
|---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner |
-| stage | no (default: `test`) | Defines a build stage |
+| 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 |
| except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
@@ -336,11 +439,18 @@ The above script will:
### artifacts
-_**Note:** Introduced in GitLab Runner v0.7.0. Also, the Windows shell executor
- does not currently support artifact uploads._
+>**Notes:**
+>
+> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
+> - Windows support was added in GitLab Runner v.1.0.0.
+> - Currently not all executors are supported.
+> - Build artifacts are only collected for successful builds.
`artifacts` is used to specify list of files and directories which should be
-attached to build after success. Below are some examples.
+attached to build after success. To pass artifacts between different builds,
+see [dependencies](#dependencies).
+
+Below are some examples.
Send all files in `binaries` and `.config`:
@@ -351,14 +461,14 @@ artifacts:
- .config
```
-Send all git untracked files:
+Send all Git untracked files:
```yaml
artifacts:
untracked: true
```
-Send all git untracked files and files in `binaries`:
+Send all Git untracked files and files in `binaries`:
```yaml
artifacts:
@@ -367,64 +477,299 @@ artifacts:
- binaries/
```
-The artifacts will be send after a successful build success to GitLab, and will
-be accessible in the GitLab UI to download.
+You may want to create artifacts only for tagged releases to avoid filling the
+build server storage with temporary build artifacts.
-### cache
+Create artifacts only for tags (`default-job` will not create artifacts):
-_**Note:** Introduced in GitLab Runner v0.7.0._
+```yaml
+default-job:
+ script:
+ - mvn test -U
+ except:
+ - tags
+
+release-job:
+ script:
+ - mvn package -U
+ artifacts:
+ paths:
+ - target/*.war
+ only:
+ - tags
+```
-`cache` is used to specify list of files and directories which should be cached
-between builds. Below are some examples:
+The artifacts will be sent to GitLab after a successful build and will
+be available for download in the GitLab UI.
-Cache all files in `binaries` and `.config`:
+#### artifacts:name
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
+
+The `name` directive allows you to define the name of the created artifacts
+archive. That way, you can have a unique name of every archive which could be
+useful when you'd like to download the archive from GitLab. The `artifacts:name`
+variable can make use of any of the [predefined variables](../variables/README.md).
+
+---
+
+**Example configurations**
+
+To create an archive with a name of the current build:
```yaml
-rspec:
- script: test
- cache:
- paths:
- - binaries/
- - .config
+job:
+ artifacts:
+ name: "$CI_BUILD_NAME"
```
-Cache all git untracked files:
+To create an archive with a name of the current branch or tag including only
+the files that are untracked by Git:
```yaml
-rspec:
- script: test
- cache:
+job:
+ artifacts:
+ name: "$CI_BUILD_REF_NAME"
+ untracked: true
+```
+
+To create an archive with a name of the current build and the current branch or
+tag including only the files that are untracked by Git:
+
+```yaml
+job:
+ artifacts:
+ name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
untracked: true
```
-Cache all git untracked files and files in `binaries`:
+To create an archive with a name of the current [stage](#stages) and branch name:
```yaml
-rspec:
- script: test
- cache:
+job:
+ artifacts:
+ name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
untracked: true
- paths:
- - binaries/
```
-Locally defined cache overwrites globally defined options. This will cache only
-`binaries/`:
+---
+
+If you use **Windows Batch** to run your shell scripts you need to replace
+`$` with `%`:
```yaml
-cache:
- paths:
- - my/files
+job:
+ artifacts:
+ name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
+ untracked: true
+```
-rspec:
- script: test
- cache:
+### dependencies
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+This feature should be used in conjunction with [`artifacts`](#artifacts) and
+allows you to define the artifacts to pass between different builds.
+
+Note that `artifacts` from previous [stages](#stages) are passed by default.
+
+To use this feature, define `dependencies` in context of the job and pass
+a list of all previous builds from which the artifacts should be downloaded.
+You can only define builds from stages that are executed before the current one.
+An error will be shown if you define builds from the current stage or next ones.
+
+---
+
+In the following example, we define two jobs with artifacts, `build:osx` and
+`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
+will be downloaded and extracted in the context of the build. The same happens
+for `test:linux` and artifacts from `build:linux`.
+
+The job `deploy` will download artifacts from all previous builds because of
+the [stage](#stages) precedence:
+
+```yaml
+build:osx:
+ stage: build
+ script: make build:osx
+ artifacts:
+ paths:
+ - binaries/
+
+build:linux:
+ stage: build
+ script: make build:linux
+ artifacts:
paths:
- binaries/
+
+test:osx:
+ stage: test
+ script: make test:osx
+ dependencies:
+ - build:osx
+
+test:linux:
+ stage: test
+ script: make test:linux
+ dependencies:
+ - build:linux
+
+deploy:
+ stage: deploy
+ script: make deploy
```
-The cache is provided on best effort basis, so don't expect that cache will be
-always present. For implementation details please check GitLab Runner.
+## Hidden jobs
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
+use this feature to ignore jobs, or use the
+[special YAML features](#special-yaml-features) and transform the hidden jobs
+into templates.
+
+In the following example, `.job_name` will be ignored:
+
+```yaml
+.job_name:
+ script:
+ - rake spec
+```
+
+## Special YAML features
+
+It's possible to use special YAML features like anchors (`&`), aliases (`*`)
+and map merging (`<<`), which will allow you to greatly reduce the complexity
+of `.gitlab-ci.yml`.
+
+Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/).
+
+### Anchors
+
+>**Note:**
+Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
+
+YAML also has a handy feature called 'anchors', which let you easily duplicate
+content across your document. Anchors can be used to duplicate/inherit
+properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
+to provide templates for your jobs.
+
+The following example uses anchors and map merging. It will create two jobs,
+`test1` and `test2`, that will inherit the parameters of `.job_template`, each
+having their own custom `script` defined:
+
+```yaml
+.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+
+test1:
+ <<: *job_definition # Merge the contents of the 'job_definition' alias
+ script:
+ - test1 project
+
+test2:
+ <<: *job_definition # Merge the contents of the 'job_definition' alias
+ script:
+ - test2 project
+```
+
+`&` sets up the name of the anchor (`job_definition`), `<<` means "merge the
+given hash into the current one", and `*` includes the named anchor
+(`job_definition` again). The expanded version looks like this:
+
+```yaml
+.job_template:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+
+test1:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+ script:
+ - test1 project
+
+test2:
+ image: ruby:2.1
+ services:
+ - postgres
+ - redis
+ script:
+ - test2 project
+```
+
+Let's see another one example. This time we will use anchors to define two sets
+of services. This will create two jobs, `test:postgres` and `test:mysql`, that
+will share the `script` directive defined in `.job_template`, and the `services`
+directive defined in `.postgres_services` and `.mysql_services` respectively:
+
+```yaml
+.job_template: &job_definition
+ script:
+ - test project
+
+.postgres_services:
+ services: &postgres_definition
+ - postgres
+ - ruby
+
+.mysql_services:
+ services: &mysql_definition
+ - mysql
+ - ruby
+
+test:postgres:
+ << *job_definition
+ services: *postgres_definition
+
+test:mysql:
+ << *job_definition
+ services: *mysql_definition
+```
+
+The expanded version looks like this:
+
+```yaml
+.job_template:
+ script:
+ - test project
+
+.postgres_services:
+ services:
+ - postgres
+ - ruby
+
+.mysql_services:
+ services:
+ - mysql
+ - ruby
+
+test:postgres:
+ script:
+ - test project
+ services:
+ - postgres
+ - ruby
+
+test:mysql:
+ script:
+ - test project
+ services:
+ - mysql
+ - ruby
+```
+
+You can see that the hidden jobs are conveniently used as templates.
## Validate the .gitlab-ci.yml
@@ -435,3 +780,10 @@ You can find the link under `/ci/lint` of your gitlab instance.
If your commit message contains `[ci skip]`, the commit will be created but the
builds will be skipped.
+
+## Examples
+
+Visit the [examples README][examples] to see a list of examples using GitLab
+CI with various languages.
+
+[examples]: ../examples/README.md
diff --git a/doc/customization/branded_login_page.md b/doc/customization/branded_login_page.md
new file mode 100644
index 00000000000..d4d9f5f7b5e
--- /dev/null
+++ b/doc/customization/branded_login_page.md
@@ -0,0 +1,19 @@
+# Changing the appearance of the login page
+
+GitLab Community Edition offers a way to put your company's identity on the login page of your GitLab server and make it a branded login page.
+
+By default, the page shows the GitLab logo and description.
+
+![default_login_page](branded_login_page/default_login_page.png)
+
+## Changing the appearance of the login page
+
+Navigate to the **Admin** area and go to the **Appearance** page.
+
+Fill in the required details like Title, Description and upload the company logo.
+
+![appearance](branded_login_page/appearance.png)
+
+After saving the page, your GitLab login page will have the details you filled in:
+
+![company_login_page](branded_login_page/custom_sign_in.png)
diff --git a/doc/customization/branded_login_page/appearance.png b/doc/customization/branded_login_page/appearance.png
new file mode 100644
index 00000000000..6bce1f0a287
--- /dev/null
+++ b/doc/customization/branded_login_page/appearance.png
Binary files differ
diff --git a/doc/customization/branded_login_page/custom_sign_in.png b/doc/customization/branded_login_page/custom_sign_in.png
new file mode 100644
index 00000000000..d6020b029a2
--- /dev/null
+++ b/doc/customization/branded_login_page/custom_sign_in.png
Binary files differ
diff --git a/doc/customization/branded_login_page/default_login_page.png b/doc/customization/branded_login_page/default_login_page.png
new file mode 100644
index 00000000000..795c7954d8e
--- /dev/null
+++ b/doc/customization/branded_login_page/default_login_page.png
Binary files differ
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index 00edfc97ed9..194b8e00299 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -16,7 +16,7 @@ Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that
For example:
```
-git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#2). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23."
+git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#22). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23."
```
will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages.
diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md
index e993230bb88..a0cb234bea0 100644
--- a/doc/customization/welcome_message.md
+++ b/doc/customization/welcome_message.md
@@ -1,12 +1,12 @@
-# Customize the complete sign-in page (GitLab Enterprise Edition only)
+# Customize the complete sign-in page
-Please see [Branded login page](http://doc.gitlab.com/ee/customization/branded_login_page.html)
+Please see [Branded login page](branded_login_page.md)
# Add a welcome message to the sign-in page (GitLab Community Edition)
It is possible to add a markdown-formatted welcome message to your GitLab
sign-in page. Users of GitLab Enterprise Edition should use the [branded login
-page feature](/ee/customization/branded_login_page.html) instead.
+page feature](branded_login_page.md) instead.
The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI.
-Admin area > Settings \ No newline at end of file
+Admin area > Settings
diff --git a/doc/development/README.md b/doc/development/README.md
index d5bf166ad32..1b281809afc 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,11 +1,12 @@
# Development
- [Architecture](architecture.md) of GitLab
-- [Shell commands](shell_commands.md) in the GitLab codebase
-- [Rake tasks](rake_tasks.md) for development
- [CI setup](ci_setup.md) for testing GitLab
+- [Gotchas](gotchas.md) to avoid
+- [How to dump production data to staging](db_dump.md)
+- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
+- [Rake tasks](rake_tasks.md) for development
+- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
+- [SQL guidelines](sql.md) for SQL guidelines
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
-- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
-- [How to dump production data to staging](dump_db.md)
-- [Benchmarking](benchmarking.md)
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 6101a71a8de..12e33406cb6 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -42,7 +42,7 @@ Gitlab-shell communicates with Sidekiq via the “communication board” (Redis)
## System Layout
-When referring to ~git in the pictures it means the home directory of the git user which is typically /home/git.
+When referring to `~git` in the pictures it means the home directory of the git user which is typically /home/git.
GitLab is primarily installed within the `/home/git` user home directory as `git` user. Within the home directory is where the gitlabhq server software resides as well as the repositories (though the repository location is configurable).
diff --git a/doc/development/benchmarking.md b/doc/development/benchmarking.md
deleted file mode 100644
index 88e18ee95f9..00000000000
--- a/doc/development/benchmarking.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# Benchmarking
-
-GitLab CE comes with a set of benchmarks that are executed for every build. This
-makes it easier to measure performance of certain components over time.
-
-Benchmarks are written as RSpec tests using a few extra helpers. To write a
-benchmark, first tag the top-level `describe`:
-
-```ruby
-describe MaruTheCat, benchmark: true do
-
-end
-```
-
-This ensures the benchmark is executed separately from other test collections.
-It also exposes the various RSpec matchers used for writing benchmarks to the
-test group.
-
-Next, lets write the actual benchmark:
-
-```ruby
-describe MaruTheCat, benchmark: true do
- let(:maru) { MaruTheChat.new }
-
- describe '#jump_in_box' do
- benchmark_subject { maru.jump_in_box }
-
- it { is_expected.to iterate_per_second(9000) }
- end
-end
-```
-
-Here `benchmark_subject` is a small wrapper around RSpec's `subject` method that
-makes it easier to specify the subject of a benchmark. Using RSpec's regular
-`subject` would require us to write the following instead:
-
-```ruby
-subject { -> { maru.jump_in_box } }
-```
-
-The `iterate_per_second` matcher defines the amount of times per second a
-subject should be executed. The higher the amount of iterations the better.
-
-By default the allowed standard deviation is a maximum of 30%. This can be
-adjusted by chaining the `with_maximum_stddev` on the `iterate_per_second`
-matcher:
-
-```ruby
-it { is_expected.to iterate_per_second(9000).with_maximum_stddev(50) }
-```
-
-This can be useful if the code in question depends on external resources of
-which the performance can vary a lot (e.g. physical HDDs, network calls, etc).
-However, in most cases 30% should be enough so only change this when really
-needed.
-
-## Benchmarks Location
-
-Benchmarks should be stored in `spec/benchmarks` and should follow the regular
-Rails specs structure. That is, model benchmarks go in `spec/benchmark/models`,
-benchmarks for code in the `lib` directory go in `spec/benchmarks/lib`, etc.
-
-## Underlying Technology
-
-The benchmark setup uses [benchmark-ips][benchmark-ips] which takes care of the
-heavy lifting such as warming up code, calculating iterations, standard
-deviation, etc.
-
-[benchmark-ips]: https://github.com/evanphx/benchmark-ips
diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md
index f9b48868182..6776d9b083f 100644
--- a/doc/development/ci_setup.md
+++ b/doc/development/ci_setup.md
@@ -26,7 +26,7 @@ We use [these build scripts](https://gitlab.com/gitlab-org/gitlab-ci/blob/master
# Build configuration on [Semaphore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for testing the [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
- Language: Ruby
-- Ruby version: 2.1.2
+- Ruby version: 2.1.8
- database.yml: pg
Build commands
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
new file mode 100644
index 00000000000..187ec9e7b75
--- /dev/null
+++ b/doc/development/doc_styleguide.md
@@ -0,0 +1,270 @@
+# Documentation styleguide
+
+This styleguide recommends best practices to improve documentation and to keep
+it organized and easy to find.
+
+## Naming
+
+- When creating a new document and it has more than one word in its name,
+ make sure to use underscores instead of spaces or dashes (`-`). For example,
+ a proper naming would be `import_projects_from_github.md`. The same rule
+ applies to images.
+
+## Text
+
+- Split up long lines, this makes it much easier to review and edit. Only
+ double line breaks are shown as a full line break in [GitLab markdown][gfm].
+ 80-100 characters is a good line length
+- Make sure that the documentation is added in the correct directory and that
+ there's a link to it somewhere useful
+- Do not duplicate information
+- Be brief and clear
+- Unless there's a logical reason not to, add documents in alphabetical order
+- Write in US English
+- Use [single spaces][] instead of double spaces
+
+## Formatting
+
+- Use dashes (`-`) for unordered lists instead of asterisks (`*`)
+- Use the number one (`1`) for ordered lists
+- Use underscores (`_`) to mark a word or text in italics
+- Use double asterisks (`**`) to mark a word or text in bold
+- When using lists, prefer not to end each item with a period. You can use
+ them if there are multiple sentences, just keep the last sentence without
+ a period
+
+## Headings
+
+- Add only one H1 title in each document, by adding `#` at the beginning of
+ it (when using markdown). For subheadings, use `##`, `###` and so on
+- Avoid putting numbers in headings. Numbers shift, hence documentation anchor
+ links shift too, which eventually leads to dead links. If you think it is
+ compelling to add numbers in headings, make sure to at least discuss it with
+ someone in the Merge Request
+- When introducing a new document, be careful for the headings to be
+ grammatically and syntactically correct. It is advised to mention one or all
+ of the following GitLab members for a review: `@axil`, `@rspeicher`,
+ `@dblessing`, `@ashleys`, `@nearlythere`. This is to ensure that no document
+ with wrong heading is going live without an audit, thus preventing dead links
+ and redirection issues when corrected
+- Leave exactly one newline after a heading
+
+## Links
+
+- If a link makes the paragraph to span across multiple lines, do not use
+ the regular Markdown approach: `[Text](https://example.com)`. Instead use
+ `[Text][identifier]` and at the very bottom of the document add:
+ `[identifier]: https://example.com`. This is another way to create Markdown
+ links which keeps the document clear and concise. Bonus points if you also
+ add an alternative text: `[identifier]: https://example.com "Alternative text"`
+ that appears when hovering your mouse on a link
+
+## Images
+
+- Place images in a separate directory named `img/` in the same directory where
+ the `.md` document that you're working on is located. Always prepend their
+ names with the name of the document that they will be included in. For
+ example, if there is a document called `twitter.md`, then a valid image name
+ could be `twitter_login_screen.png`.
+- Images should have a specific, non-generic name that will differentiate them.
+- Keep all file names in lower case.
+- Consider using PNG images instead of JPEG.
+
+Inside the document:
+
+- The Markdown way of using an image inside a document is:
+ `![Proper description what the image is about](img/document_image_title.png)`
+- Always use a proper description for what the image is about. That way, when a
+ browser fails to show the image, this text will be used as an alternative
+ description
+- If there are consecutive images with little text between them, always add
+ three dashes (`---`) between the image and the text to create a horizontal
+ line for better clarity
+- If a heading is placed right after an image, always add three dashes (`---`)
+ between the image and the heading
+
+## Notes
+
+- Notes should be quoted with the word `Note:` being bold. Use this form:
+
+ ```
+ >**Note:**
+ This is something to note.
+ ```
+
+ which renders to:
+
+ >**Note:**
+ This is something to note.
+
+ If the note spans across multiple lines it's OK to split the line.
+
+## New features
+
+- Every piece of documentation that comes with a new feature should declare the
+ GitLab version that feature got introduced. Right below the heading add a
+ note: `_**Note:** This feature was introduced in GitLab 8.3_`
+- If possible every feature should have a link to the MR that introduced it.
+ The above note would be then transformed to:
+ `_**Note:** This feature was [introduced][ce-1242] in GitLab 8.3_`, where
+ the [link identifier](#links) is named after the repository (CE) and the MR
+ number
+- If the feature is only in GitLab EE, don't forget to mention it, like:
+ `_**Note:** This feature was introduced in GitLab EE 8.3_`. Otherwise, leave
+ this mention out
+
+## References
+
+- **GitLab Restart:**
+ There are many cases that a restart/reconfigure of GitLab is required. To
+ avoid duplication, link to the special document that can be found in
+ [`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will
+ read like:
+
+ ```
+ Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
+ for the changes to take effect.
+ ```
+ If the document you are editing resides in a place other than the GitLab CE/EE
+ `doc/` directory, instead of the relative link, use the full path:
+ `http://doc.gitlab.com/ce/administration/restart_gitlab.html`.
+ Replace `reconfigure` with `restart` where appropriate.
+
+## Installation guide
+
+- **Ruby:**
+ In [step 2 of the installation guide](../install/installation.md#2-ruby),
+ we install Ruby from source. Whenever there is a new version that needs to
+ be updated, remember to change it throughout the codeblock and also replace
+ the sha256sum (it can be found in the [downloads page][ruby-dl] of the Ruby
+ website).
+
+[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website"
+
+## API
+
+Here is a list of must-have items. Use them in the exact order that appears
+on this document. Further explanation is given below.
+
+- Every method must have the REST API request. For example:
+
+ ```
+ GET /projects/:id/repository/branches
+ ```
+
+- Every method must have a detailed
+ [description of the parameters](#method-description).
+- Every method must have a cURL example.
+- Every method must have a response body (in JSON format).
+
+### Method description
+
+Use the following table headers to describe the methods. Attributes should
+always be in code blocks using backticks (`).
+
+```
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+```
+
+Rendered example:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user` | string | yes | The GitLab username |
+
+### cURL commands
+
+- Use `https://gitlab.example.com/api/v3/` as an endpoint.
+- Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`.
+- Always put the request first. `GET` is the default so you don't have to
+ include it.
+- Use double quotes to the URL when it includes additional parameters.
+- Prefer to use examples using the private token and don't pass data of
+ username and password.
+
+| Methods | Description |
+| ------- | ----------- |
+| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed |
+| `-X POST` | Use this method when creating new objects |
+| `-X PUT` | Use this method when updating existing objects |
+| `-X DELETE` | Use this method when removing existing objects |
+
+### cURL Examples
+
+Below is a set of [cURL][] examples that you can use in the API documentation.
+
+#### Simple cURL command
+
+Get the details of a group:
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org
+```
+
+#### cURL example with parameters passed in the URL
+
+Create a new project under the authenticated user's namespace:
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo"
+```
+
+#### Post data using cURL's --data
+
+Instead of using `-X POST` and appending the parameters to the URI, you can use
+cURL's `--data` option. The example below will create a new project `foo` under
+the authenticated user's namespace.
+
+```bash
+curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+```
+
+#### Post data using JSON content
+
+_**Note:** In this example we create a new group. Watch carefully the single
+and double quotes._
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
+```
+
+#### Post data using form-data
+
+Instead of using JSON or urlencode you can use multipart/form-data which
+properly handles data encoding:
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "title=ssh-key" -F "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys
+```
+
+The above example is run by and administrator and will add an SSH public key
+titled ssh-key to user's account which has an id of 25.
+
+#### Escape special characters
+
+Spaces or slashes (`/`) may sometimes result to errors, thus it is recommended
+to escape them when possible. In the example below we create a new issue which
+contains spaces in its title. Observe how spaces are escaped using the `%20`
+ASCII code.
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude"
+```
+
+Use `%2F` for slashes (`/`).
+
+#### Pass arrays to API calls
+
+The GitLab API sometimes accepts arrays of strings or integers. For example, to
+restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
+`example.net`, you would do something like this:
+
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domains[]=*.example.com" -d "restricted_signup_domains[]=example.net" https://gitlab.example.com/api/v3/application/settings
+```
+
+[cURL]: http://curl.haxx.se/ "cURL website"
+[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
+[gfm]: http://doc.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation"
+[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
new file mode 100644
index 00000000000..21078c8d6f9
--- /dev/null
+++ b/doc/development/gotchas.md
@@ -0,0 +1,103 @@
+# Gotchas
+
+The purpose of this guide is to document potential "gotchas" that contributors
+might encounter or should avoid during development of GitLab CE and EE.
+
+## Don't `describe` symbols
+
+Consider the following model spec:
+
+```ruby
+require 'rails_helper'
+
+describe User do
+ describe :to_param do
+ it 'converts the username to a param' do
+ user = described_class.new(username: 'John Smith')
+
+ expect(user.to_param).to eq 'john-smith'
+ end
+ end
+end
+```
+
+When run, this spec doesn't do what we might expect:
+
+```sh
+spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMethodError: undefined method `new' for :to_param:Symbol
+```
+
+### Solution
+
+Except for the top-level `describe` block, always provide a String argument to
+`describe`.
+
+## Don't `rescue Exception`
+
+See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
+
+_**Note:** This rule is [enforced automatically by
+Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L911-914)._
+
+[Exception]: http://stackoverflow.com/q/10048173/223897
+
+## Don't use inline CoffeeScript in views
+
+Using the inline `:coffee` or `:coffeescript` Haml filters comes with a
+performance overhead.
+
+_**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-5-stable/config/initializers/haml.rb)
+in an initializer._
+
+### Further reading
+
+- Pull Request: [Replace CoffeeScript block into JavaScript in Views](https://git.io/vztMu)
+- Stack Overflow: [Performance implications of using :coffescript filter inside HAML templates?](http://stackoverflow.com/a/17571242/223897)
+
+## ID-based CSS selectors need to be a bit more specific
+
+Normally, because HTML `id` attributes need to be unique to the page, it's
+perfectly fine to write some JavaScript like the following:
+
+```javascript
+$('#js-my-selector').hide();
+```
+
+However, there's a feature of GitLab's Markdown processing that [automatically
+adds anchors to header elements][ToC Processing], with the `id` attribute being
+automatically generated based on the content of the header.
+
+Unfortunately, this feature makes it possible for user-generated content to
+create a header element with the same `id` attribute we're using in our
+selector, potentially breaking the JavaScript behavior. A user could break the
+above example with the following Markdown:
+
+```markdown
+## JS My Selector
+```
+
+Which gets converted to the following HTML:
+
+```html
+<h2>
+ <a id="js-my-selector" class="anchor" href="#js-my-selector" aria-hidden="true"></a>
+ JS My Selector
+</h2>
+```
+
+[ToC Processing]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/lib/banzai/filter/table_of_contents_filter.rb#L31-37
+
+### Solution
+
+The current recommended fix for this is to make our selectors slightly more
+specific:
+
+```javascript
+$('div#js-my-selector').hide();
+```
+
+### Further reading
+
+- Issue: [Merge request ToC anchor conflicts with tabs](https://gitlab.com/gitlab-org/gitlab-ce/issues/3908)
+- Merge Request: [Make tab target selectors less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2023)
+- Merge Request: [Make cross-project reference's clipboard target less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2024)
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 4fa1961fde9..28dedf3978c 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -8,12 +8,14 @@ In addition, having to take a server offline for a an upgrade small or big is
a big burden for most organizations. For this reason it is important that your
migrations are written carefully, can be applied online and adhere to the style guide below.
+It's advised to have offline migrations only in major GitLab releases.
+
When writing your migrations, also consider that databases might have stale data
or inconsistencies and guard for that. Try to make as little assumptions as possible
about the state of the database.
Please don't depend on GitLab specific code since it can change in future versions.
-If needed copy-paste GitLab code into the migration to make make it forward compatible.
+If needed copy-paste GitLab code into the migration to make it forward compatible.
## Comments in the migration
@@ -33,6 +35,8 @@ It is always preferable to have a migration run online. If you expect the migrat
to take particularly long (for instance, if it loops through all notes),
this is valuable information to add.
+If you don't provide the information it means that a migration is safe to run online.
+
### Reversibility
Your migration should be reversible. This is very important, as it should
@@ -85,4 +89,4 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i
execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})")
execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
end
-```
+``` \ No newline at end of file
diff --git a/doc/development/scss_styleguide.md b/doc/development/scss_styleguide.md
new file mode 100644
index 00000000000..6c48c25448b
--- /dev/null
+++ b/doc/development/scss_styleguide.md
@@ -0,0 +1,194 @@
+# SCSS styleguide
+
+This style guide recommends best practices for SCSS to make styles easy to read,
+easy to maintain, and performant for the end-user.
+
+## Rules
+
+### Naming
+
+CSS classes should use the `lowercase-hyphenated` format rather than
+`snake_case` or `camelCase`.
+
+```scss
+// Bad
+.class_name {
+ color: #fff;
+}
+
+// Bad
+.className {
+ color: #fff;
+}
+
+// Good
+.class-name {
+ color: #fff;
+}
+```
+
+### Formatting
+
+You should always use a space before a brace, braces should be on the same
+line, each property should each get its own line, and there should be a space
+between the property and its value.
+
+```scss
+// Bad
+.container-item {
+ width: 100px; height: 100px;
+ margin-top: 0;
+}
+
+// Bad
+.container-item
+{
+ width: 100px;
+ height: 100px;
+ margin-top: 0;
+}
+
+// Bad
+.container-item{
+ width:100px;
+ height:100px;
+ margin-top:0;
+}
+
+// Good
+.container-item {
+ width: 100px;
+ height: 100px;
+ margin-top: 0;
+}
+```
+
+Note that there is an exception for single-line rulesets, although these are
+not typically recommended.
+
+```scss
+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`.
+
+```scss
+// Bad
+p {
+ color: #ffffff;
+}
+
+// Bad
+p {
+ color: #FFFFFF;
+}
+
+// Good
+p {
+ color: #fff;
+}
+```
+
+### Indentation
+
+Indentation should always use two spaces for each indentation level.
+
+```scss
+// Bad, four spaces
+p {
+ color: #f00;
+}
+
+// Good
+p {
+ color: #f00;
+}
+```
+
+### Semicolons
+
+Always include semicolons after every property. When the stylesheets are
+minified, the semicolons will be removed automatically.
+
+```scss
+// Bad
+.container-item {
+ width: 100px;
+ height: 100px
+}
+
+// Good
+.container-item {
+ width: 100px;
+ height: 100px;
+}
+```
+
+### Shorthand
+
+The shorthand form should be used for properties that support it.
+
+```scss
+// Bad
+margin: 10px 15px 10px 15px;
+padding: 10px 10px 10px 10px;
+
+// Good
+margin: 10px 15px;
+padding: 10px;
+```
+
+### Zero Units
+
+Omit length units on zero values, they're unnecessary and not including them
+is slightly more performant.
+
+```scss
+// Bad
+.item-with-padding {
+ padding: 0px;
+}
+
+// Good
+.item-with-padding {
+ padding: 0;
+}
+```
+
+### 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.
+
+## Linting
+
+We use [SCSS Lint][scss-lint] to check for style guide conformity. It uses the
+ruleset in `.scss-lint.yml`, which is located in the home directory of the
+project.
+
+To check if any warnings will be produced by your changes, you can run `rake
+scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to
+catch any warnings.
+
+If the Rake task is throwing warnings you don't understand, SCSS Lint's
+documentation includes [a full list of their linters][scss-lint-documentation].
+
+### Fixing issues
+
+If you want to automate changing a large portion of the codebase to conform to
+the SCSS style guide, you can use [CSSComb][csscomb]. First install
+[Node][node] and [NPM][npm], then run `npm install csscomb -g` to install
+CSSComb globally (system-wide). Run it in the GitLab directory with
+`csscomb app/assets/stylesheets` to automatically fix issues with CSS/SCSS.
+
+Note that this won't fix every problem, but it should fix a majority.
+
+[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
diff --git a/doc/development/sql.md b/doc/development/sql.md
new file mode 100644
index 00000000000..23fd7604957
--- /dev/null
+++ b/doc/development/sql.md
@@ -0,0 +1,219 @@
+# SQL Query Guidelines
+
+This document describes various guidelines to follow when writing SQL queries,
+either using ActiveRecord/Arel or raw SQL queries.
+
+## Using LIKE Statements
+
+The most common way to search for data is using the `LIKE` statement. For
+example, to get all issues with a title starting with "WIP:" you'd write the
+following query:
+
+```sql
+SELECT *
+FROM issues
+WHERE title LIKE 'WIP:%';
+```
+
+On PostgreSQL the `LIKE` statement is case-sensitive. On MySQL this depends on
+the case-sensitivity of the collation, which is usually case-insensitive. To
+perform a case-insensitive `LIKE` on PostgreSQL you have to use `ILIKE` instead.
+This statement in turn isn't supported on MySQL.
+
+To work around this problem you should write `LIKE` queries using Arel instead
+of raw SQL fragments as Arel automatically uses `ILIKE` on PostgreSQL and `LIKE`
+on MySQL. This means that instead of this:
+
+```ruby
+Issue.where('title LIKE ?', 'WIP:%')
+```
+
+You'd write this instead:
+
+```ruby
+Issue.where(Issue.arel_table[:title].matches('WIP:%'))
+```
+
+Here `matches` generates the correct `LIKE` / `ILIKE` statement depending on the
+database being used.
+
+If you need to chain multiple `OR` conditions you can also do this using Arel:
+
+```ruby
+table = Issue.arel_table
+
+Issue.where(table[:title].matches('WIP:%').or(table[:foo].matches('WIP:%')))
+```
+
+For PostgreSQL this produces:
+
+```sql
+SELECT *
+FROM issues
+WHERE (title ILIKE 'WIP:%' OR foo ILIKE 'WIP:%')
+```
+
+In turn for MySQL this produces:
+
+```sql
+SELECT *
+FROM issues
+WHERE (title LIKE 'WIP:%' OR foo LIKE 'WIP:%')
+```
+
+## LIKE & Indexes
+
+Neither PostgreSQL nor MySQL use any indexes when using `LIKE` / `ILIKE` with a
+wildcard at the start. For example, this will not use any indexes:
+
+```sql
+SELECT *
+FROM issues
+WHERE title ILIKE '%WIP:%';
+```
+
+Because the value for `ILIKE` starts with a wildcard the database is not able to
+use an index as it doesn't know where to start scanning the indexes.
+
+MySQL provides no known solution to this problem. Luckily PostgreSQL _does_
+provide a solution: trigram GIN indexes. These indexes can be created as
+follows:
+
+```sql
+CREATE INDEX [CONCURRENTLY] index_name_here
+ON table_name
+USING GIN(column_name gin_trgm_ops);
+```
+
+The key here is the `GIN(column_name gin_trgm_ops)` part. This creates a [GIN
+index][gin-index] with the operator class set to `gin_trgm_ops`. These indexes
+_can_ be used by `ILIKE` / `LIKE` and can lead to greatly improved performance.
+One downside of these indexes is that they can easily get quite large (depending
+on the amount of data indexed).
+
+To keep naming of these indexes consistent please use the following naming
+pattern:
+
+ index_TABLE_on_COLUMN_trigram
+
+For example, a GIN/trigram index for `issues.title` would be called
+`index_issues_on_title_trigram`.
+
+Due to these indexes taking quite some time to be built they should be built
+concurrently. This can be done by using `CREATE INDEX CONCURRENTLY` instead of
+just `CREATE INDEX`. Concurrent indexes can _not_ be created inside a
+transaction. Transactions for migrations can be disabled using the following
+pattern:
+
+```ruby
+class MigrationName < ActiveRecord::Migration
+ disable_ddl_transaction!
+end
+```
+
+For example:
+
+```ruby
+class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));'
+ execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));'
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ remove_index :users, :index_on_users_lower_username
+ remove_index :users, :index_on_users_lower_email
+ end
+end
+```
+
+## Plucking IDs
+
+This can't be stressed enough: **never** use ActiveRecord's `pluck` to pluck a
+set of values into memory only to use them as an argument for another query. For
+example, this will make the database **very** sad:
+
+```ruby
+projects = Project.all.pluck(:id)
+
+MergeRequest.where(source_project_id: projects)
+```
+
+Instead you can just use sub-queries which perform far better:
+
+```ruby
+MergeRequest.where(source_project_id: Project.all.select(:id))
+```
+
+The _only_ time you should use `pluck` is when you actually need to operate on
+the values in Ruby itself (e.g. write them to a file). In almost all other cases
+you should ask yourself "Can I not just use a sub-query?".
+
+## Use UNIONs
+
+UNIONs aren't very commonly used in most Rails applications but they're very
+powerful and useful. In most applications queries tend to use a lot of JOINs to
+get related data or data based on certain criteria, but JOIN performance can
+quickly deteriorate as the data involved grows.
+
+For example, if you want to get a list of projects where the name contains a
+value _or_ the name of the namespace contains a value most people would write
+the following query:
+
+```sql
+SELECT *
+FROM projects
+JOIN namespaces ON namespaces.id = projects.namespace_id
+WHERE projects.name ILIKE '%gitlab%'
+OR namespaces.name ILIKE '%gitlab%';
+```
+
+Using a large database this query can easily take around 800 milliseconds to
+run. Using a UNION we'd write the following instead:
+
+```sql
+SELECT projects.*
+FROM projects
+WHERE projects.name ILIKE '%gitlab%'
+
+UNION
+
+SELECT projects.*
+FROM projects
+JOIN namespaces ON namespaces.id = projects.namespace_id
+WHERE namespaces.name ILIKE '%gitlab%';
+```
+
+This query in turn only takes around 15 milliseconds to complete while returning
+the exact same records.
+
+This doesn't mean you should start using UNIONs everywhere, but it's something
+to keep in mind when using lots of JOINs in a query and filtering out records
+based on the joined data.
+
+GitLab comes with a `Gitlab::SQL::Union` class that can be used to build a UNION
+of multiple `ActiveRecord::Relation` objects. You can use this class as
+follows:
+
+```ruby
+union = Gitlab::SQL::Union.new([projects, more_projects, ...])
+
+Project.from("(#{union.to_sql}) projects")
+```
+
+## Ordering by Creation Date
+
+When ordering records based on the time they were created you can simply order
+by the `id` column instead of ordering by `created_at`. Because IDs are always
+unique and incremented in the order that rows are created this will produce the
+exact same results. This also means there's no need to add an index on
+`created_at` to ensure consistent performance as `id` is already indexed by
+default.
+
+[gin-index]: http://www.postgresql.org/docs/current/static/gin.html
diff --git a/doc/gitlab-basics/basicsimages/compare_braches.png b/doc/gitlab-basics/basicsimages/compare_branches.png
index 7eebaed9075..7eebaed9075 100644
--- a/doc/gitlab-basics/basicsimages/compare_braches.png
+++ b/doc/gitlab-basics/basicsimages/compare_branches.png
Binary files differ
diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
index 0f2665a3bf7..dcdf49d3379 100644
--- a/doc/hooks/custom_hooks.md
+++ b/doc/hooks/custom_hooks.md
@@ -2,7 +2,7 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
+Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 86d205ba7a5..4cfb8402943 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -74,10 +74,11 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
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 for the changes to take effect:
+1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh
sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart mailroom
```
1. Verify that everything is configured correctly:
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
index 18bf3db1744..787d21f7f8f 100644
--- a/doc/incoming_email/postfix.md
+++ b/doc/incoming_email/postfix.md
@@ -84,7 +84,12 @@ The instructions make the assumption that you will be using the email address `i
quit
```
- (Note: The `.` is a literal period on its own line)
+ _**Note:** The `.` is a literal period on its own line._
+
+ _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
+ then your Postfix `my_network` configuration is not correct. The error will
+ say 'Temporary lookup failure'. See
+ [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
1. Check if the `incoming` user received the email:
@@ -131,7 +136,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
1. Test the new setup:
1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
- 2. Check if the `incoming` user received the email:
+ 1. Check if the `incoming` user received the email:
```sh
su - incoming
@@ -152,6 +157,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
q
```
+ _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
+ version of `mail` doesn't support Maildir style mailboxes. Install
+ `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
+ try the above steps again, substituting `heirloom-mailx` for the `mail`
+ command._
+
1. Log out of the `incoming` account and go back to being `root`:
```sh
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index 513ad69ec26..e51ff5a5de2 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -8,7 +8,7 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se
# Install the database packages
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
-
+
# Ensure you have MySQL version 5.5.14 or later
mysql --version
@@ -31,7 +31,7 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se
# Ensure you can use the InnoDB engine which is necessary to support long indexes
# If this fails, check your MySQL config files (e.g. `/etc/mysql/*.cnf`, `/etc/mysql/conf.d/*`) for the setting "innodb = off"
mysql> SET storage_engine=INNODB;
-
+
# Create the GitLab production database
mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
@@ -52,3 +52,25 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se
mysql> \q
# You are done installing the database and can go back to the rest of the installation.
+
+## MySQL strings limits
+
+After installation or upgrade, remember to run the `add_limits_mysql` Rake task:
+
+```
+bundle exec rake add_limits_mysql
+```
+
+The `text` type in MySQL has a different size limit than the `text` type in
+PostgreSQL. In MySQL `text` columns are limited to ~65kB, whereas in PostgreSQL
+`text` columns are limited up to ~1GB!
+
+The `add_limits_mysql` Rake task converts some important `text` columns in the
+GitLab database to `longtext` columns, which can persist values of up to 4GB
+(sometimes less if the value contains multibyte characters).
+
+Details can be found in the [PostgreSQL][postgres-text-type] and
+[MySQL][mysql-text-types] manuals.
+
+[postgres-text-type]: http://www.postgresql.org/docs/9.1/static/datatype-character.html
+[mysql-text-types]: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 81edd8da2b8..c567846f624 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -22,7 +22,7 @@ If the highest number stable branch is unclear please check the [GitLab Blog](ht
This guide is long because it covers many cases and includes all commands you need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880).
-This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [doc/install/requirements.md](./requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/).
+This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [requirements.md](requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/).
This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options please see [the installation section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation).
@@ -76,7 +76,7 @@ Make sure you have the right version of Git installed
# Install Git
sudo apt-get install -y git-core
- # Make sure Git is version 1.7.10 or higher, for example 1.7.12 or 2.0.0
+ # Make sure Git is version 2.7.4 or higher
git --version
Is the system packaged Git too old? Remove it and compile from source.
@@ -89,8 +89,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.4.3.tar.gz | tar xz
- cd git-2.4.3/
+ curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz
+ echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz
+ cd git-2.7.4/
./configure
make prefix=/usr/local all
@@ -107,18 +108,25 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby
-The use of Ruby version managers such as [RVM](https://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby.
+_**Note:** The current supported Ruby version is 2.1.x. Ruby 2.2 and 2.3 are
+currently not supported._
-Remove the old Ruby 1.8 if present
+The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
+in production, frequently leads to hard to diagnose problems. For example,
+GitLab Shell is called from OpenSSH, and having a version manager can prevent
+pushing and pulling over SSH. Version managers are not supported and we strongly
+advise everyone to follow the instructions below to use a system Ruby.
+
+Remove the old Ruby 1.8 if present:
sudo apt-get remove ruby1.8
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.7.tar.gz
- echo 'e2e195a4a58133e3ad33b955c829bb536fa3c075 ruby-2.1.7.tar.gz' | shasum -c - && tar xzf ruby-2.1.7.tar.gz
- cd ruby-2.1.7
+ curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.8.tar.gz
+ echo 'c7e50159357afd87b13dc5eaf4ac486a70011149 ruby-2.1.8.tar.gz' | shasum -c - && tar xzf ruby-2.1.8.tar.gz
+ cd ruby-2.1.8
./configure --disable-install-rdoc
make
sudo make install
@@ -135,11 +143,11 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you
use 64-bit Linux. You can find downloads for other platforms at the [Go download
page](https://golang.org/dl).
- curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz
- echo '46eecd290d8803887dec718c691cc243f2175fe0 go1.5.1.linux-amd64.tar.gz' | shasum -c - && \
- sudo tar -C /usr/local -xzf go1.5.1.linux-amd64.tar.gz
+ curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
+ echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
- rm go1.5.1.linux-amd64.tar.gz
+ rm go1.5.3.linux-amd64.tar.gz
## 4. System Users
@@ -154,18 +162,11 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Install the database packages
sudo apt-get install -y postgresql postgresql-client libpq-dev
- # Login to PostgreSQL
- sudo -u postgres psql -d template1
-
# Create a user for GitLab
- # Do not type the 'template1=#', this is part of the prompt
- template1=# CREATE USER git CREATEDB;
+ sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;"
# Create the GitLab production database & grant all privileges on database
- template1=# CREATE DATABASE gitlabhq_production OWNER git;
-
- # Quit the database session
- template1=# \q
+ sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;"
# Try connecting to the new database with the new user
sudo -u git -H psql -d gitlabhq_production
@@ -175,25 +176,20 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
## 6. Redis
-As of this writing, most Debian/Ubuntu distributions ship with Redis 2.2 or
-2.4. GitLab requires at least Redis 2.8.
-
-Ubuntu users [can use a PPA](https://launchpad.net/~chris-lea/+archive/ubuntu/redis-server)
-to install a recent version of Redis.
+GitLab requires at least Redis 2.8.
-The following instructions cover building and installing Redis from scratch:
+If you are using Debian 8 or Ubuntu 14.04 and up, then you can simply install
+Redis 2.8 with:
```sh
-# Build Redis
-wget http://download.redis.io/releases/redis-2.8.23.tar.gz
-tar xzf redis-2.8.23.tar.gz
-cd redis-2.8.23
-make
+sudo apt-get install redis-server
+```
-# Install Redis
-cd utils
-sudo ./install_server.sh
+If you are using Debian 7 or Ubuntu 12.04, follow the special documentation
+on [an alternate Redis installation](redis.md). Once done, follow the rest of
+the guide here.
+```
# Configure redis to use sockets
sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig
@@ -217,7 +213,7 @@ if [ -d /etc/tmpfiles.d ]; then
fi
# Activate the changes to redis.conf
-sudo service redis_6379 start
+sudo service redis-server restart
# Add git to the redis group
sudo usermod -aG redis git
@@ -231,9 +227,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-3-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab
-**Note:** You can change `8-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -260,8 +256,12 @@ sudo usermod -aG redis git
sudo chmod -R u+rwX tmp/pids/
sudo chmod -R u+rwX tmp/sockets/
- # Make sure GitLab can write to the public/uploads/ directory
- sudo chmod -R u+rwX public/uploads
+ # Create the public/uploads/ directory
+ sudo -u git -H mkdir public/uploads/
+
+ # Make sure only the GitLab user has access to the public/uploads/ directory
+ # now that files in public/uploads are served by gitlab-workhorse
+ sudo chmod 0700 public/uploads
# Change the permissions of the directory where CI build traces are stored
sudo chmod -R u+rwX builds/
@@ -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.5.1
+ sudo -u git -H git checkout 0.6.5
sudo -u git -H make
### Initialize Database and Activate Advanced Features
@@ -363,9 +363,9 @@ GitLab Shell is an SSH access and repository management software developed speci
# When done you see 'Administrator account created:'
-**Note:** You can set the Administrator/root password by supplying it in environmental variable `GITLAB_ROOT_PASSWORD` as seen below. If you don't set the password (and it is set to the default one) please wait with exposing GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login you'll be forced to change the default password.
+**Note:** You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one) please wait with exposing GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login you'll be forced to change the default password.
- sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword
+ sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail
### Secure secrets.yml
@@ -461,12 +461,15 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj
### Initial Login
-Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created a default admin account for you. You can use it to log in:
+Visit YOUR_SERVER in your web browser for your first GitLab login.
- root
- 5iveL!fe
+If you didn't [provide a root password during setup](#initialize-database-and-activate-advanced-features),
+you'll be redirected to a password reset screen to provide the password for the
+initial administrator account. Enter your desired password and you'll be
+redirected back to the login screen.
-**Important Note:** On login you'll be prompted to change the password.
+The default account's username is **root**. Provide the password you created
+earlier and login. After login you can change the username if you wish.
**Enjoy!**
@@ -474,6 +477,11 @@ You can use `sudo service gitlab start` and `sudo service gitlab stop` to start
## Advanced Setup Tips
+### Relative URL support
+
+See the [Relative URL documentation](relative_url.md) for more information on
+how to configure GitLab with a relative URL.
+
### Using HTTPS
To use GitLab with HTTPS:
@@ -552,6 +560,10 @@ Apart from the always supported markdown style there are other rich text files t
If you see this message when attempting to clone a repository hosted by GitLab,
this is likely due to an outdated Nginx or Apache configuration, or a missing or
-misconfigured `gitlab-git-http-server` instance. Double-check that you've
-[installed Go](#3-go), [installed gitlab-git-http-server](#install-gitlab-git-http-server),
+misconfigured gitlab-workhorse instance. Double-check that you've
+[installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse),
and correctly [configured Nginx](#site-configuration).
+
+[RVM]: https://rvm.io/ "RVM Homepage"
+[rbenv]: https://github.com/sstephenson/rbenv "rbenv on GitHub"
+[chruby]: https://github.com/postmodern/chruby "chruby on GitHub"
diff --git a/doc/install/redis.md b/doc/install/redis.md
new file mode 100644
index 00000000000..4075e6283d0
--- /dev/null
+++ b/doc/install/redis.md
@@ -0,0 +1,60 @@
+# Install Redis on old distributions
+
+GitLab requires at least Redis 2.8. The following guide is for Debian 7 and
+Ubuntu 12.04. If you are using Debian 8 or Ubuntu 14.04 and up, follow the
+[installation guide](installation.md).
+
+## Install Redis 2.8 in Debian 7
+
+Redis 2.8 is included in the Debian Wheezy [backports] repository.
+
+1. Edit `/etc/apt/sources.list` and add the following line:
+
+ ```
+ deb http://http.debian.net/debian wheezy-backports main
+ ```
+
+1. Update the repositories:
+
+ ```
+ sudo apt-get update
+ ```
+
+1. Install `redis-server`:
+
+ ```
+ sudo apt-get -t wheezy-backports install redis-server
+ ```
+
+1. Follow the rest of the [installation guide](installation.md).
+
+## Install Redis 2.8 in Ubuntu 12.04
+
+We will [use a PPA](https://launchpad.net/~chris-lea/+archive/ubuntu/redis-server)
+to install a recent version of Redis.
+
+1. Install the PPA repository:
+
+ ```
+ sudo add-apt-repository ppa:chris-lea/redis-server
+ ```
+
+ Your system will now fetch the PPA's key. This enables your Ubuntu system to
+ verify that the packages in the PPA have not been interfered with since they
+ were built.
+
+1. Update the repositories:
+
+ ```
+ sudo apt-get update
+ ```
+
+1. Install `redis-server`:
+
+ ```
+ sudo apt-get install redis-server
+ ```
+
+1. Follow the rest of the [installation guide](installation.md).
+
+[backports]: http://backports.debian.org/Instructions/ "Debian backports website"
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
new file mode 100644
index 00000000000..0245febfcd8
--- /dev/null
+++ b/doc/install/relative_url.md
@@ -0,0 +1,136 @@
+## Install GitLab under a relative URL
+
+_**Note:**
+This document describes how to run GitLab under a relative URL for installations
+from source. If you are using an Omnibus package,
+[the steps are different][omnibus-rel]. Use this guide along with the
+[installation guide](installation.md) if you are installing GitLab for the
+first time._
+
+---
+
+While it is recommended to install GitLab on its own (sub)domain, sometimes
+this is not possible due to a variety of reasons. In that case, GitLab can also
+be installed under a relative URL, for example `https://example.com/gitlab`.
+
+There is no limit to how deeply nested the relative URL can be. For example you
+could serve GitLab under `/foo/bar/gitlab/git` without any issues.
+
+Note that by changing the URL on an existing GitLab installation, all remote
+URLs will change, so you'll have to manually edit them in any local repository
+that points to your GitLab instance.
+
+---
+
+The TL;DR list of configuration files that you need to change in order to
+serve GitLab under a relative URL is:
+
+- `/home/git/gitlab/config/initializers/relative_url.rb`
+- `/home/git/gitlab/config/gitlab.yml`
+- `/home/git/gitlab/config/unicorn.rb`
+- `/home/git/gitlab-shell/config.yml`
+- `/etc/default/gitlab`
+
+After all the changes you need to recompile the assets and [restart GitLab].
+
+### Relative URL requirements
+
+If you configure GitLab with a relative URL, the assets (JavaScript, CSS, fonts,
+images, etc.) will need to be recompiled, which is a task which consumes a lot
+of CPU and memory resources. To avoid out-of-memory errors, you should have at
+least 2GB of RAM available on your system, while we recommend 4GB RAM, and 4 or
+8 CPU cores.
+
+See the [requirements](requirements.md) document for more information.
+
+### Enable relative URL in GitLab
+
+_**Note:**
+Do not make any changes to your web server configuration file regarding
+relative URL. The relative URL support is implemented by GitLab Workhorse._
+
+---
+
+Before following the steps below to enable relative URL in GitLab, some
+assumptions are made:
+
+- GitLab is served under `/gitlab`
+- The directory under which GitLab is installed is `/home/git/`
+
+Make sure to follow all steps below:
+
+1. (Optional) If you run short on resources, you can temporarily free up some
+ memory by shutting down the GitLab service with the following command:
+
+ ```shell
+ sudo service gitlab stop
+ ```
+
+1. Create `/home/git/gitlab/config/initializers/relative_url.rb`
+
+ ```shell
+ cp /home/git/gitlab/config/initializers/relative_url.rb.sample \
+ /home/git/gitlab/config/initializers/relative_url.rb
+ ```
+
+ and change the following line:
+
+ ```ruby
+ config.relative_url_root = "/gitlab"
+ ```
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and uncomment/change the
+ following line:
+
+ ```yaml
+ relative_url_root: /gitlab
+ ```
+
+1. Edit `/home/git/gitlab/config/unicorn.rb` and uncomment/change the
+ following line:
+
+ ```ruby
+ ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab"
+ ```
+
+1. Edit `/home/git/gitlab-shell/config.yml` and append the relative path to
+ the following line:
+
+ ```yaml
+ gitlab_url: http://127.0.0.1/gitlab
+ ```
+
+1. Make sure you have copied the supplied init script and the defaults file
+ as stated in the [installation guide](installation.md#install-init-script).
+ Then, edit `/etc/default/gitlab` and set in `gitlab_workhorse_options` the
+ `-authBackend` setting to read like:
+
+ ```shell
+ -authBackend http://127.0.0.1:8080/gitlab
+ ```
+
+ **Note:**
+ If you are using a custom init script, make sure to edit the above
+ gitlab-workhorse setting as needed.
+
+1. After all the above changes recompile the assets. This is an important task
+ and will take some time to complete depending on the server resources:
+
+ ```
+ cd /home/git/gitlab
+ sudo -u git -H bundle exec rake assets:clean assets:precompile RAILS_ENV=production
+ ```
+
+1. [Restart GitLab][] for the changes to take effect.
+
+### Disable relative URL in GitLab
+
+To disable the relative URL:
+
+1. Remove `/home/git/gitlab/config/initializers/relative_url.rb`
+
+1. Follow the same as above starting from 2. and set up the
+ GitLab URL to one that doesn't contain a relative path.
+
+[omnibus-rel]: http://doc.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab"
+[restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab"
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index c0ccdd37458..03cb08dd1f1 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -22,7 +22,7 @@ For the installations options please see [the installation page on the GitLab we
- FreeBSD
On the above unsupported distributions is still possible to install GitLab yourself.
-Please see the [installation from source guide](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/installation.md) and the [unofficial installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) on the public wiki for more information.
+Please see the [installation from source guide](installation.md) and the [installation guides](https://about.gitlab.com/installation/) for more information.
### Non-Unix operating systems such as Windows
@@ -32,15 +32,17 @@ Please consider using a virtual machine to run GitLab.
## Ruby versions
-GitLab requires Ruby (MRI) 2.1
+GitLab requires Ruby (MRI) 2.1.x and currently does not work with versions 2.2 or 2.3.
+
You will have to use the standard MRI implementation of Ruby.
-We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab needs several Gems that have native extensions.
+We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab
+needs several Gems that have native extensions.
## Hardware requirements
### Storage
-The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least as much free space as all your repos combined take up.
+The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least as much free space as all your repos combined take up.
If you want to be flexible about growing your hard drive space in the future consider mounting it using LVM so you can add more hard drives when you need them.
@@ -64,9 +66,9 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim
You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab!
With less memory GitLab will give strange errors during the reconfigure run and 500 errors during usage.
-- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advise.
-- 1GB RAM + 1GB swap supports up to 100 users but it will be slow
-- **2GB RAM** is the **recommended** memory size and supports up to 100 users
+- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice.
+- 1GB RAM + 1GB swap supports up to 100 users but it will be very slow
+- **2GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
- 4GB RAM supports up to 1,000 users
- 8GB RAM supports up to 2,000 users
- 16GB RAM supports up to 4,000 users
@@ -95,6 +97,17 @@ To change the Unicorn workers when you have the Omnibus package please see [the
If you want to run the database separately expect a size of about 1 MB per user.
+### PostgreSQL Requirements
+
+Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
+GitLab database. This extension can be enabled (using a PostgreSQL super user)
+by running the following query for every database:
+
+ CREATE EXTENSION pg_trgm;
+
+On some systems you may need to install an additional package (e.g.
+`postgresql-contrib`) for this extension to become available.
+
## Redis and Sidekiq
Redis stores all user sessions and the background task queue.
@@ -109,4 +122,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o
- Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/))
- Safari 7+ (known problem: required fields in html5 do not work)
- Opera (Latest released version)
-- Internet Explorer (IE) 10+ but please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file
+- Internet Explorer (IE) 10+ but please make sure that you have the `Compatibility View` mode disabled.
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 2a9f76533b7..7c8f785a61f 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -1,27 +1,72 @@
# GitLab Integration
-GitLab integrates with multiple third-party services to allow external issue trackers and external authentication.
+GitLab integrates with multiple third-party services to allow external issue
+trackers and external authentication.
See the documentation below for details on how to configure these services.
-- [Jira](jira.md) Integrate with the JIRA issue tracker
+- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
-- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth.
+- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](cas.md) Configure GitLab to sign in using CAS
- [Slack](slack.md) Integrate with the Slack chat service
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
+- [Akismet](akismet.md) Configure Akismet to stop spam
-GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html).
+GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
## Project services
-Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a Project Service.
-You can find these within GitLab in the Services page under Project Settings if you are at least a master on the project.
-Project Services are a bit like plugins in that they allow a lot of freedom in adding functionality to GitLab, for example there is also a service that can send an email every time someone pushes new commits.
-Because GitLab is open source we can ship with the code and tests for all plugins.
-This allows the community to keep the plugins up to date so that they always work in newer GitLab versions.
-For an overview of what projects services are available without logging in please see the [project_services directory](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services).
+Integration with services such as Campfire, Flowdock, Gemnasium, HipChat,
+Pivotal Tracker, and Slack are available in the form of a [Project Service][].
+You can find these within GitLab in the Services page under Project Settings if
+you are at least a master on the project.
+Project Services are a bit like plugins in that they allow a lot of freedom in
+adding functionality to GitLab. For example there is also a service that can
+send an email every time someone pushes new commits.
+
+Because GitLab is open source we can ship with the code and tests for all
+plugins. This allows the community to keep the plugins up to date so that they
+always work in newer GitLab versions.
+
+For an overview of what projects services are available without logging in,
+please see the [project_services directory][projects-code].
+
+[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html
+[Project Service]: ../project_services/project_services.md
+[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
+
+## SSL certificate errors
+
+When trying to integrate GitLab with services that are using self-signed certificates,
+it is very likely that SSL certificate errors will occur on different parts of the
+application, most likely Sidekiq. There are 2 approaches you can take to solve this:
+
+1. Add the root certificate to the trusted chain of the OS.
+1. If using Omnibus, you can add the certificate to GitLab's trusted certificates.
+
+**OS main trusted chain**
+
+This [resource](http://kb.kerio.com/product/kerio-connect/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html)
+has all the information you need to add a certificate to the main trusted chain.
+
+This [answer](http://superuser.com/questions/437330/how-do-you-add-a-certificate-authority-ca-to-ubuntu)
+at SuperUser also has relevant information.
+
+**Omnibus Trusted Chain**
+
+It is enough to concatenate the certificate to the main trusted certificate:
+
+```bash
+cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem
+```
+
+After that restart GitLab with:
+
+```bash
+sudo gitlab-ctl restart
+```
diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md
new file mode 100644
index 00000000000..5cc09bd536d
--- /dev/null
+++ b/doc/integration/akismet.md
@@ -0,0 +1,30 @@
+# Akismet
+
+GitLab leverages [Akismet](http://akismet.com) to protect against spam. Currently
+GitLab uses Akismet to prevent users who are not members of a project from
+creating spam via the GitLab API. Detected spam will be rejected, and
+an entry in the "Spam Log" section in the Admin page will be created.
+
+Privacy note: GitLab submits the user's IP and user agent to Akismet. Note that
+adding a user to a project will disable the Akismet check and prevent this
+from happening.
+
+## Configuration
+
+To use Akismet:
+
+1. Go to the URL: https://akismet.com/account/
+
+2. Sign-in or create a new account.
+
+3. Click on "Show" to reveal the API key.
+
+4. Go to Applications Settings on Admin Area (`admin/application_settings`)
+
+5. Check the `Enable Akismet` checkbox
+
+6. Fill in the API key from step 3.
+
+7. Save the configuration.
+
+![Screenshot of Akismet settings](img/akismet_settings.png)
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
new file mode 100644
index 00000000000..e5247082a89
--- /dev/null
+++ b/doc/integration/auth0.md
@@ -0,0 +1,89 @@
+# Auth0 OmniAuth Provider
+
+To enable the Auth0 OmniAuth provider, you must create an Auth0 account, and an
+application.
+
+1. Sign in to the [Auth0 Console](https://manage.auth0.com). If you need to
+create an account, you can do so at the same link.
+
+1. Select "New App/API".
+
+1. Provide the Application Name ('GitLab' works fine).
+
+1. Once created, you should see the Quick Start options. Disregard them and
+select 'Settings' above the Quick Start options.
+
+1. At the top of the Settings screen, you should see your Domain, Client ID and
+Client Secret. Take note of these as you'll need to put them in the
+configuration file. For example:
+ - Domain: `test1234.auth0.com`
+ - Client ID: `t6X8L2465bNePWLOvt9yi41i`
+ - Client Secret: `KbveM3nqfjwCbrhaUy_gDu2dss8TIlHIdzlyf33pB7dEK5u_NyQdp65O_o02hXs2`
+
+1. Fill in the Allowed Callback URLs:
+ - http://`YOUR_GITLAB_URL`/users/auth/auth0/callback (or)
+ - https://`YOUR_GITLAB_URL`/users/auth/auth0/callback
+
+1. Fill in the Allowed Origins (CORS):
+ - http://`YOUR_GITLAB_URL` (or)
+ - https://`YOUR_GITLAB_URL`
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
+for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "auth0",
+ "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'',
+ client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
+ namespace: 'YOUR_AUTH0_DOMAIN'
+ }
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```yaml
+ - { name: 'auth0',
+ args: {
+ client_id: 'YOUR_AUTH0_CLIENT_ID',
+ client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
+ namespace: 'YOUR_AUTH0_DOMAIN'
+ }
+ }
+ ```
+
+1. Change `YOUR_AUTH0_CLIENT_ID` to the client ID from the Auth0 Console page
+from step 5.
+
+1. Change `YOUR_AUTH0_CLIENT_SECRET` to the client secret from the Auth0 Console
+page from step 5.
+
+1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
+for the changes to take effect.
+
+On the sign in page there should now be an Auth0 icon below the regular sign in
+form. Click the icon to begin the authentication process. Auth0 will ask the
+user to sign in and authorize the GitLab application. If everything goes well
+the user will be returned to GitLab and will be signed in.
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
new file mode 100644
index 00000000000..48dddf7df44
--- /dev/null
+++ b/doc/integration/azure.md
@@ -0,0 +1,83 @@
+# Microsoft Azure OAuth2 OmniAuth Provider
+
+To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your application with Azure. Azure will generate a client ID and secret key for you to use.
+
+1. Sign in to the [Azure Management Portal](https://manage.windowsazure.com>).
+
+1. Select "Active Directory" on the left and choose the directory you want to use to register GitLab.
+
+1. Select "Applications" at the top bar and click the "Add" button the bottom.
+
+1. Select "Add an application my organization is developing".
+
+1. Provide the project information and click the "Next" button.
+ - Name: 'GitLab' works just fine here.
+ - Type: 'WEB APPLICATION AND/OR WEB API'
+
+1. On the "App properties" page enter the needed URI's and click the "Complete" button.
+ - SIGN-IN URL: Enter the URL of your GitLab installation (e.g 'https://gitlab.mycompany.com/')
+ - APP ID URI: Enter the endpoint URL for Microsoft to use, just has to be unique (e.g 'https://mycompany.onmicrosoft.com/gitlab')
+
+1. Select "Configure" in the top menu.
+
+1. Add a "Reply URL" pointing to the Azure OAuth callback of your GitLab installation (e.g. https://gitlab.mycompany.com/users/auth/azure_oauth2/callback).
+
+1. Create a "Client secret" by selecting a duration, the secret will be generated as soon as you click the "Save" button in the bottom menu..
+
+1. Note the "CLIENT ID" and the "CLIENT SECRET".
+
+1. Select "View endpoints" from the bottom menu.
+
+1. You will see lots of endpoint URLs in the form 'https://login.microsoftonline.com/TENANT ID/...', note down the TENANT ID part of one of those endpoints.
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "azure_oauth2",
+ "args" => {
+ "client_id" => "CLIENT ID",
+ "client_secret" => "CLIENT SECRET",
+ "tenant_id" => "TENANT ID",
+ }
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```
+ - { name: 'azure_oauth2',
+ args: { client_id: "CLIENT ID",
+ client_secret: "CLIENT SECRET",
+ tenant_id: "TENANT ID" } }
+ ```
+
+1. Replace 'CLIENT ID', 'CLIENT SECRET' and 'TENANT ID' with the values you got above.
+
+1. Save the configuration file.
+
+1. Restart GitLab for the changes to take effect.
+
+On the sign in page there should now be a Microsoft icon below the regular sign in form. Click the icon to begin the authentication process. Microsoft will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 3e660cfba1e..a2d7e922aad 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -1,44 +1,30 @@
# External issue tracker
-GitLab has a great issue tracker but you can also use an external issue tracker such as Jira, Bugzilla or Redmine. You can configure issue trackers per GitLab project. For instance, if you configure Jira it allows you to do the following:
+GitLab has a great issue tracker but you can also use an external one such as
+Jira or Redmine. Issue trackers are configurable per GitLab project and allow
+you to do the following:
-- the 'Issues' link on the GitLab project pages takes you to the appropriate Jira issue index;
-- clicking 'New issue' on the project dashboard creates a new Jira issue;
-- To reference Jira issue PROJECT-1234 in comments, use syntax PROJECT-1234. Commit messages get turned into HTML links to the corresponding Jira issue.
-
-![Jira screenshot](jira-integration-points.png)
-
-GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html).
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ issue index of the external tracker
+- clicking **New issue** on the project dashboard creates a new issue on the
+ external tracker
## Configuration
-### Project Service
+The configuration is done via a project's **Services**.
-You can enable an external issue tracker per project. As an example, we will configure `Redmine` for project named gitlab-ci.
-
-Fill in the required details on the page:
+### Project Service
-![redmine configuration](redmine_configuration.png)
+To enable an external issue tracker you must configure the appropriate **Service**.
+Visit the links below for details:
-* `description` A name for the issue tracker (to differentiate between instances, for example).
-* `project_url` The URL to the project in Redmine which is being linked to this GitLab project.
-* `issues_url` The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the url. This id is used by GitLab as a placeholder to replace the issue number.
-* `new_issue_url` This is the URL to create a new issue in Redmine for the project linked to this GitLab project.
+- [Redmine](../project_services/redmine.md)
+- [Jira](../project_services/jira.md)
### Service Template
-It is necessary to configure the external issue tracker per project, because project specific details are needed for the integration with GitLab.
-The admin can add a service template that sets a default for each project. This makes it much easier to configure individual projects.
-
-In GitLab Admin section, navigate to `Service Templates` and choose the service template you want to create:
-
-![redmine service template](redmine_service_template.png)
-
-After the template is created, the template details will be pre-filled on the project service page.
-
-NOTE: For each project, you will still need to configure the issue tracking URLs by replacing `:issues_tracker_id` in the above screenshot
-with the ID used by your external issue tracker. Prior to GitLab v7.8, this ID was configured in the project settings, and GitLab would automatically
-update the URL configured in `gitlab.yml`. This behavior is now depecated, and all issue tracker URLs must be configured directly
-within the project's Services settings.
+To save you the hassle from configuring each project's service individually,
+GitLab provides the ability to set Service Templates which can then be
+overridden in each project's settings.
-Support to add your commits to the Jira ticket automatically is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jira.html).
+Read more on [Services Templates](../project_services/services_templates.md).
diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md
index bc1f1673086..77bb75cbfca 100644
--- a/doc/integration/facebook.md
+++ b/doc/integration/facebook.md
@@ -19,7 +19,7 @@ something else descriptive.
1. Enter the address of your GitLab installation at the bottom of the package
- ![Facebook Website URL](facebook_website_url.png)
+ ![Facebook Website URL](img/facebook_website_url.png)
1. Choose "Next"
@@ -29,7 +29,7 @@ something else descriptive.
1. Fill in a contact email for your app
- ![Facebook App Settings](facebook_app_settings.png)
+ ![Facebook App Settings](img/facebook_app_settings.png)
1. Choose "Save Changes"
@@ -45,7 +45,7 @@ something else descriptive.
1. You should now see an app key and app secret (see screenshot). Keep this page open as you continue configuration.
- ![Facebook API Keys](facebook_api_keys.png)
+ ![Facebook API Keys](img/facebook_api_keys.png)
1. On your GitLab server, open the configuration file.
diff --git a/doc/integration/github.md b/doc/integration/github.md
index a789d2c814f..886784a27c9 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -22,7 +22,7 @@ GitHub will generate an application ID and secret key for you to use.
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
Keep this page open as you continue configuration.
- ![GitHub app](github_app.png)
+ ![GitHub app](img/github_app.png)
1. On your GitLab server, open the configuration file.
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index 80e3c0142a0..b215cc7c609 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -28,7 +28,7 @@ GitLab.com will generate an application ID and secret key for you to use.
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
Keep this page open as you continue configuration.
- ![GitLab app](gitlab_app.png)
+ ![GitLab app](img/gitlab_app.png)
1. On your GitLab server, open the configuration file.
diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md
index de45f25ad62..05a91d9bef9 100644
--- a/doc/integration/gmail_action_buttons_for_gitlab.md
+++ b/doc/integration/gmail_action_buttons_for_gitlab.md
@@ -4,7 +4,7 @@ GitLab supports [Google actions in email](https://developers.google.com/gmail/ma
If correctly setup, emails that require an action will be marked in Gmail.
-![gmail_actions_button.png](gmail_actions_button.png)
+![gmail_actions_button.png](img/gmail_action_buttons_for_gitlab.png)
To get this functioning, you need to be registered with Google.
[See how to register with Google in this document.](https://developers.google.com/gmail/markup/registering-with-google)
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 91e9b2495cc..f9a20dd840d 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -25,7 +25,7 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
- Application type: "Web Application"
- Authorized JavaScript origins: This isn't really used by GitLab but go ahead and put 'https://gitlab.example.com' here.
- Authorized redirect URI: 'https://gitlab.example.com/users/auth/google_oauth2/callback'
-1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](google_app.png)
+1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](img/google_app.png)
1. On your GitLab server, open the configuration file.
diff --git a/doc/integration/img/akismet_settings.png b/doc/integration/img/akismet_settings.png
new file mode 100644
index 00000000000..ccdd3adb1c5
--- /dev/null
+++ b/doc/integration/img/akismet_settings.png
Binary files differ
diff --git a/doc/integration/facebook_api_keys.png b/doc/integration/img/facebook_api_keys.png
index d6c44ac0f11..d6c44ac0f11 100644
--- a/doc/integration/facebook_api_keys.png
+++ b/doc/integration/img/facebook_api_keys.png
Binary files differ
diff --git a/doc/integration/facebook_app_settings.png b/doc/integration/img/facebook_app_settings.png
index 30dd21e198a..30dd21e198a 100644
--- a/doc/integration/facebook_app_settings.png
+++ b/doc/integration/img/facebook_app_settings.png
Binary files differ
diff --git a/doc/integration/facebook_website_url.png b/doc/integration/img/facebook_website_url.png
index dc3088bb2fa..dc3088bb2fa 100644
--- a/doc/integration/facebook_website_url.png
+++ b/doc/integration/img/facebook_website_url.png
Binary files differ
diff --git a/doc/integration/github_app.png b/doc/integration/img/github_app.png
index d890345ced9..d890345ced9 100644
--- a/doc/integration/github_app.png
+++ b/doc/integration/img/github_app.png
Binary files differ
diff --git a/doc/integration/gitlab_app.png b/doc/integration/img/gitlab_app.png
index 3f9391a821b..3f9391a821b 100644
--- a/doc/integration/gitlab_app.png
+++ b/doc/integration/img/gitlab_app.png
Binary files differ
diff --git a/doc/integration/gmail_actions_button.png b/doc/integration/img/gmail_action_buttons_for_gitlab.png
index b08f54d137b..b08f54d137b 100644
--- a/doc/integration/gmail_actions_button.png
+++ b/doc/integration/img/gmail_action_buttons_for_gitlab.png
Binary files differ
diff --git a/doc/integration/google_app.png b/doc/integration/img/google_app.png
index 5a62ad35009..5a62ad35009 100644
--- a/doc/integration/google_app.png
+++ b/doc/integration/img/google_app.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_admin_application.png b/doc/integration/img/oauth_provider_admin_application.png
new file mode 100644
index 00000000000..a2d8e14c120
--- /dev/null
+++ b/doc/integration/img/oauth_provider_admin_application.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_application_form.png b/doc/integration/img/oauth_provider_application_form.png
new file mode 100644
index 00000000000..3a676b22393
--- /dev/null
+++ b/doc/integration/img/oauth_provider_application_form.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_application_id_secret.png b/doc/integration/img/oauth_provider_application_id_secret.png
new file mode 100644
index 00000000000..6d68df001af
--- /dev/null
+++ b/doc/integration/img/oauth_provider_application_id_secret.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_authorized_application.png b/doc/integration/img/oauth_provider_authorized_application.png
new file mode 100644
index 00000000000..efc3b807d71
--- /dev/null
+++ b/doc/integration/img/oauth_provider_authorized_application.png
Binary files differ
diff --git a/doc/integration/img/oauth_provider_user_wide_applications.png b/doc/integration/img/oauth_provider_user_wide_applications.png
new file mode 100644
index 00000000000..45ad8a6d468
--- /dev/null
+++ b/doc/integration/img/oauth_provider_user_wide_applications.png
Binary files differ
diff --git a/doc/integration/twitter_app_api_keys.png b/doc/integration/img/twitter_app_api_keys.png
index 1076337172a..1076337172a 100644
--- a/doc/integration/twitter_app_api_keys.png
+++ b/doc/integration/img/twitter_app_api_keys.png
Binary files differ
diff --git a/doc/integration/twitter_app_details.png b/doc/integration/img/twitter_app_details.png
index b95e8af8a74..b95e8af8a74 100644
--- a/doc/integration/twitter_app_details.png
+++ b/doc/integration/img/twitter_app_details.png
Binary files differ
diff --git a/doc/integration/jira-integration-points.png b/doc/integration/jira-integration-points.png
deleted file mode 100644
index 0692a7b458a..00000000000
--- a/doc/integration/jira-integration-points.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index 624601d0fac..78aa6634116 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1,113 +1,3 @@
-# GitLab Jira integration
+# GitLab JIRA integration
-GitLab can be configured to interact with Jira.
-Configuration happens via username and password.
-Connecting to a Jira server via CAS is not possible.
-
-Each project can be configured to connect to a different Jira instance, configuration is explained [here](#configuration).
-If you have one Jira instance you can pre-fill the settings page with a default template. To configure the template [see external issue tracker document](external-issue-tracker.md#service-template)).
-
-Once the project is connected to Jira, you can reference and close the issues in Jira directly from GitLab.
-
-
-## Table of Contents
-
-* [Referencing Jira Issues from GitLab](#referencing-jira-issues)
-* [Closing Jira Issues from GitLab](#closing-jira-issues)
-* [Configuration](#configuration)
-
-### Referencing Jira Issues
-
-When GitLab project has Jira issue tracker configured and enabled, mentioning Jira issue in GitLab will automatically add a comment in Jira issue with the link back to GitLab. This means that in comments in merge requests and commits referencing an issue, eg. `PROJECT-7`, will add a comment in Jira issue in the format:
-
-
-```
- USER mentioned this issue in LINK_TO_THE_MENTION
-```
-
-* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
-* `LINK_TO_THE_MENTION` Link to the origin of mention with a name of the entity where Jira issue was mentioned.
-Can be commit or merge request.
-
-
-![example of mentioning or closing the Jira issue](jira_issue_reference.png)
-
-
-### Closing Jira Issues
-
-Jira issues can be closed directly from GitLab by using trigger words, eg. `Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and merge requests.
-When a commit which contains the trigger word in the commit message is pushed, GitLab will add a comment in the mentioned Jira issue.
-
-For example, for project named PROJECT in Jira, we implemented a new feature and created a merge request in GitLab.
-
-This feature was requested in Jira issue PROJECT-7. Merge request in GitLab contains the improvement and in merge request description we say that this merge request `Closes PROJECT-7` issue.
-
-Once this merge request is merged, Jira issue will be automatically closed with a link to the commit that resolved the issue.
-
-![A Git commit that causes the Jira issue to be closed](merge_request_close_jira.png)
-
-
-![The GitLab integration user leaves a comment on Jira](jira_service_close_issue.png)
-
-
-## Configuration
-
-### Configuring JIRA
-
-We need to create a user in JIRA which will have access to all projects that need to integrate with GitLab.
-Login to your JIRA instance as admin and under Administration go to User Management and create a new user.
-As an example, we'll create a user named `gitlab` and add it to `jira-developers` group.
-
-**It is important that the user `gitlab` has write-access to projects in JIRA**
-
-### Configuring GitLab
-
-### GitLab 7.8 EE and up with JIRA v6.x
-
-To enable JIRA integration in a project, navigate to the project Settings page and go to Services. Here you will find JIRA.
-
-Fill in the required details on the page:
-
-![Jira service page](jira_service_page.png)
-
-* `description` A name for the issue tracker (to differentiate between instances, for instance).
-* `project url` The URL to the JIRA project which is being linked to this GitLab project.
-* `issues url` The URL to the JIRA project issues overview for the project that is linked to this GitLab project.
-* `new issue url` This is the URL to create a new issue in JIRA for the project linked to this GitLab project.
-* `api url` The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`.
-* `username` The username of the user created in [configuring JIRA step](#configuring-jira).
-* `password` The password of the user created in [configuring JIRA step](#configuring-jira).
-* `Jira issue transition` This is the id of a transition that moves issues to a closed state. You can find this number under [JIRA workflow administration, see screenshot](jira_workflow_screenshot.png). By default, this id is `2`. (In the example image, this is `2` as well)
-
-After saving the configuration, your GitLab project will be able to interact with the linked JIRA project.
-
-
-### GitLab 6.x-7.7 with JIRA v6.x
-
-**Note: GitLab 7.8 and up contain various integration improvements. We strongly recommend upgrading.**
-
-
-In `gitlab.yml` enable [JIRA issue tracker section by uncommenting the lines](https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115).
-This will make sure that all issues within GitLab are pointing to the JIRA issue tracker.
-
-We can also enable JIRA service that will allow us to interact with JIRA issues.
-
-For example, we can close issues in JIRA by a commit in GitLab.
-
-Go to project settings page and fill in the project name for the JIRA project:
-
-![Set the JIRA project name in GitLab to 'NEW'](jira_project_name.png)
-
-Next, go to the services page and find JIRA.
-
-![Jira services page](jira_service.png)
-
-1. Tick the active check box to enable the service.
-1. Supply the url to JIRA server, for example http://jira.sample
-1. Supply the username of a user we created under `Configuring JIRA` section, for example `gitlab`
-1. Supply the password of the user
-1. Optional: supply the JIRA api version, default is version
-1. Optional: supply the JIRA issue transition ID (issue transition to closed). This is dependant on JIRA settings, default is 2
-1. Save
-
-Now we should be able to interact with JIRA issues.
+This document was moved under [project_services/jira](../project_services/jira.md).
diff --git a/doc/integration/jira_service_page.png b/doc/integration/jira_service_page.png
deleted file mode 100644
index 69ec44e826f..00000000000
--- a/doc/integration/jira_service_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 845f588f913..cf1f98492ea 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -48,6 +48,11 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
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.
@@ -199,3 +204,25 @@ 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
+```
+
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index 192c321f712..5f8bb57365c 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -1,35 +1,80 @@
-## GitLab as OAuth2 authentication service provider
+# GitLab as OAuth2 authentication service provider
-This document is about using GitLab as an OAuth authentication service provider to sign into other services.
-If you want to use other OAuth authentication service providers to sign into GitLab please see the [OAuth2 client documentation](../api/oauth2.md)
+This document is about using GitLab as an OAuth authentication service provider
+to sign in to other services.
-OAuth2 provides client applications a 'secure delegated access' to server resources on behalf of a resource owner. Or you can allow users to sign in to your application with their GitLab.com account.
-In fact OAuth allows to issue access token to third-party clients by an authorization server,
-with the approval of the resource owner, or end-user.
-Mostly, OAuth2 is using for SSO (Single sign-on). But you can find a lot of different usages for this functionality.
-For example, our feature 'GitLab Importer' is using OAuth protocol to give an access to repositories without sharing user credentials to GitLab.com account.
-Also GitLab.com application can be used for authentication to your GitLab instance if needed [GitLab OmniAuth](gitlab.md).
+If you want to use other OAuth authentication service providers to sign in to
+GitLab, please see the [OAuth2 client documentation](../api/oauth2.md).
-GitLab has two ways to add new OAuth2 application to an instance, you can add application as regular user and through admin area. So GitLab actually can have an instance-wide and a user-wide applications. There is no defferences between them except the different permission levels.
+## Introduction to OAuth
-### Adding application through profile
-Go to your profile section 'Application' and press button 'New Application'
+[OAuth] provides to client applications a 'secure delegated access' to server
+resources on behalf of a resource owner. In fact, OAuth allows an authorization
+server to issue access tokens to third-party clients with the approval of the
+resource owner, or the end-user.
-![applications](oauth_provider/user_wide_applications.png)
+OAuth is mostly used as a Single Sign-On service (SSO), but you can find a
+lot of different uses for this functionality. For example, you can allow users
+to sign in to your application with their GitLab.com account, or GitLab.com
+can be used for authentication to your GitLab instance
+(see [GitLab OmniAuth](gitlab.md)).
-After this you will see application form, where "Name" is arbitrary name, "Redirect URI" is URL in your app where users will be sent after authorization on GitLab.com.
+The 'GitLab Importer' feature is also using the OAuth protocol to give access
+to repositories without sharing user credentials to your GitLab.com account.
-![application_form](oauth_provider/application_form.png)
+---
-### Authorized application
-Every application you authorized will be shown in your "Authorized application" sections.
+GitLab supports two ways of adding a new OAuth2 application to an instance. You
+can either add an application as a regular user or add it in the admin area.
+What this means is that GitLab can actually have instance-wide and a user-wide
+applications. There is no difference between them except for the different
+permission levels they are set (user/admin).
-![authorized_application](oauth_provider/authorized_application.png)
+## Adding an application through the profile
-At any time you can revoke access just clicking button "Revoke"
+In order to add a new application via your profile, navigate to
+**Profile Settings > Applications** and select **New Application**.
-### OAuth applications in admin area
+![New OAuth application](img/oauth_provider_user_wide_applications.png)
-If you want to create application that does not belong to certain user you can create it from admin area
+---
-![admin_application](oauth_provider/admin_application.png) \ No newline at end of file
+In the application form, enter a **Name** (arbitrary), and make sure to set up
+correctly the **Redirect URI** which is the URL where users will be sent after
+they authorize with GitLab.
+
+![New OAuth application form](img/oauth_provider_application_form.png)
+
+---
+
+When you hit **Submit** you will be provided with the application ID and
+the application secret which you can then use with your application that
+connects to GitLab.
+
+![OAuth application ID and secret](img/oauth_provider_application_id_secret.png)
+
+---
+
+## OAuth applications in the admin area
+
+To create an application that does not belong to a certain user, you can create
+it from the admin area.
+
+![OAuth admin_applications](img/oauth_provider_admin_application.png)
+
+---
+
+## Authorized applications
+
+Every application you authorized to use your GitLab credentials will be shown
+in the **Authorized applications** section under **Profile Settings > Applications**.
+
+![Authorized_applications](img/oauth_provider_authorized_application.png)
+
+---
+
+As you can see, the default scope `api` is used, which is the only scope that
+GitLab supports so far. At any time you can revoke any access by just clicking
+**Revoke**.
+
+[oauth]: http://oauth.net/2/ "OAuth website"
diff --git a/doc/integration/oauth_provider/admin_application.png b/doc/integration/oauth_provider/admin_application.png
deleted file mode 100644
index a5f34512aa8..00000000000
--- a/doc/integration/oauth_provider/admin_application.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/oauth_provider/application_form.png b/doc/integration/oauth_provider/application_form.png
deleted file mode 100644
index ae135db2627..00000000000
--- a/doc/integration/oauth_provider/application_form.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/oauth_provider/authorized_application.png b/doc/integration/oauth_provider/authorized_application.png
deleted file mode 100644
index d3ce05be9cc..00000000000
--- a/doc/integration/oauth_provider/authorized_application.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/oauth_provider/user_wide_applications.png b/doc/integration/oauth_provider/user_wide_applications.png
deleted file mode 100644
index 719e1974068..00000000000
--- a/doc/integration/oauth_provider/user_wide_applications.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index f2b1721fc03..25f35988305 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -1,27 +1,56 @@
# OmniAuth
-GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and other popular services.
+GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and
+other popular services.
-Configuring OmniAuth does not prevent standard GitLab authentication or LDAP (if configured) from continuing to work. Users can choose to sign in using any of the configured mechanisms.
+Configuring OmniAuth does not prevent standard GitLab authentication or LDAP
+(if configured) from continuing to work. Users can choose to sign in using any
+of the configured mechanisms.
- [Initial OmniAuth Configuration](#initial-omniauth-configuration)
- [Supported Providers](#supported-providers)
- [Enable OmniAuth for an Existing User](#enable-omniauth-for-an-existing-user)
- [OmniAuth configuration sample when using Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master#omniauth-google-twitter-github-login)
+## Supported Providers
+
+This is a list of the current supported OmniAuth providers. Before proceeding
+on each provider's documentation, make sure to first read this document as it
+contains some settings that are common for all providers.
+
+- [GitHub](github.md)
+- [Bitbucket](bitbucket.md)
+- [GitLab.com](gitlab.md)
+- [Google](google.md)
+- [Facebook](facebook.md)
+- [Twitter](twitter.md)
+- [Shibboleth](shibboleth.md)
+- [SAML](saml.md)
+- [Crowd](crowd.md)
+- [Azure](azure.md)
+- [Auth0](auth0.md)
+
## Initial OmniAuth Configuration
-Before configuring individual OmniAuth providers there are a few global settings that are in common for all providers that we need to consider.
+Before configuring individual OmniAuth providers there are a few global settings
+that are in common for all providers that we need to consider.
- Omniauth needs to be enabled, see details below for example.
-- `allow_single_sign_on` defaults to `false`. If `false` users must be created manually or they will not be able to
-sign in via OmniAuth.
-- `block_auto_created_users` defaults to `true`. If `true` auto created users will be blocked by default and will
-have to be unblocked by an administrator before they are able to sign in.
-- **Note:** If you set `allow_single_sign_on` to `true` and `block_auto_created_users` to `false` please be aware
-that any user on the Internet will be able to successfully sign in to your GitLab without administrative approval.
-
-If you want to change these settings:
+- `allow_single_sign_on` allows you to specify the providers you want to allow to
+ automatically create an account. It defaults to `false`. If `false` users must
+ be created manually or they will not be able to sign in via OmniAuth.
+- `block_auto_created_users` defaults to `true`. If `true` auto created users will
+ be blocked by default and will have to be unblocked by an administrator before
+ they are able to sign in.
+
+>**Note:**
+If you set `block_auto_created_users` to `false`, make sure to only
+define providers under `allow_single_sign_on` that you are able to control, like
+SAML, Shibboleth, Crowd or Google, or set it to `false` otherwise any user on
+the Internet will be able to successfully sign in to your GitLab without
+administrative approval.
+
+To change these settings:
* **For omnibus package**
@@ -31,11 +60,16 @@ If you want to change these settings:
sudo editor /etc/gitlab/gitlab.rb
```
- and change
+ and change:
- ```
+ ```ruby
gitlab_rails['omniauth_enabled'] = true
- gitlab_rails['omniauth_allow_single_sign_on'] = false
+
+ # CAUTION!
+ # This allows users to login without having a user account first. Define the allowed providers
+ # using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none.
+ # User accounts will be created automatically when authentication was successful.
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['saml', 'twitter']
gitlab_rails['omniauth_block_auto_created_users'] = true
```
@@ -49,55 +83,57 @@ If you want to change these settings:
sudo -u git -H editor config/gitlab.yml
```
- and change the following section
+ and change the following section:
- ```
+ ```yaml
## OmniAuth settings
omniauth:
# Allow login via Twitter, Google, etc. using OmniAuth providers
enabled: true
# CAUTION!
- # This allows users to login without having a user account first (default: false).
+ # This allows users to login without having a user account first. Define the allowed providers
+ # using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none.
# User accounts will be created automatically when authentication was successful.
- allow_single_sign_on: false
+ allow_single_sign_on: ["saml", "twitter"]
+
# Locks down those users until they have been cleared by the admin (default: true).
block_auto_created_users: true
```
-Now we can choose one or more of the Supported Providers below to continue configuration.
-
-## Supported Providers
-
-- [GitHub](github.md)
-- [Bitbucket](bitbucket.md)
-- [GitLab.com](gitlab.md)
-- [Google](google.md)
-- [Facebook](facebook.md)
-- [Twitter](twitter.md)
-- [Shibboleth](shibboleth.md)
-- [SAML](saml.md)
-- [Crowd](crowd.md)
+Now we can choose one or more of the Supported Providers listed above to continue
+the configuration process.
## Enable OmniAuth for an Existing User
-Existing users can enable OmniAuth for specific providers after the account is created. For example, if the user originally signed in with LDAP an OmniAuth provider such as Twitter can be enabled. Follow the steps below to enable an OmniAuth provider for an existing user.
+Existing users can enable OmniAuth for specific providers after the account is
+created. For example, if the user originally signed in with LDAP, an OmniAuth
+provider such as Twitter can be enabled. Follow the steps below to enable an
+OmniAuth provider for an existing user.
1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider.
1. Go to profile settings (the silhouette icon in the top right corner).
1. Select the "Account" tab.
1. Under "Connected Accounts" select the desired OmniAuth provider, such as Twitter.
-1. The user will be redirected to the provider. Once the user authorized GitLab they will be redirected back to GitLab.
+1. The user will be redirected to the provider. Once the user authorized GitLab
+ they will be redirected back to GitLab.
The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on.
## Using Custom Omniauth Providers
-GitLab uses [Omniauth](http://www.omniauth.org/) for authentication and already ships with a few providers preinstalled (e.g. LDAP, GitHub, Twitter). But sometimes that is not enough and you need to integrate with other authentication solutions. For these cases you can use the Omniauth provider.
+>**Note:**
+The following information only applies for installations from source.
+
+GitLab uses [Omniauth](http://www.omniauth.org/) for authentication and already ships
+with a few providers pre-installed (e.g. LDAP, GitHub, Twitter). But sometimes that
+is not enough and you need to integrate with other authentication solutions. For
+these cases you can use the Omniauth provider.
### Steps
-These steps are fairly general and you will need to figure out the exact details from the Omniauth provider's documentation.
+These steps are fairly general and you will need to figure out the exact details
+from the Omniauth provider's documentation.
- Stop GitLab:
@@ -123,8 +159,12 @@ These steps are fairly general and you will need to figure out the exact details
### Examples
-If you have successfully set up a provider that is not shipped with GitLab itself, please let us know.
+If you have successfully set up a provider that is not shipped with GitLab itself,
+please let us know.
-You can help others by reporting successful configurations and probably share a few insights or provide warnings for common errors or pitfalls by sharing your experience [in the public Wiki](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Custom-omniauth-provider-configurations).
+You can help others by reporting successful configurations and probably share a
+few insights or provide warnings for common errors or pitfalls by sharing your
+experience [in the public Wiki](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Custom-omniauth-provider-configurations).
-While we can't officially support every possible authentication mechanism out there, we'd like to at least help those with specific needs.
+While we can't officially support every possible authentication mechanism out there,
+we'd like to at least help those with specific needs.
diff --git a/doc/integration/redmine_configuration.png b/doc/integration/redmine_configuration.png
deleted file mode 100644
index 6b145363229..00000000000
--- a/doc/integration/redmine_configuration.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/redmine_service_template.png b/doc/integration/redmine_service_template.png
deleted file mode 100644
index 1159eb5b964..00000000000
--- a/doc/integration/redmine_service_template.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 1632e42f701..1c3dc707f6d 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -1,10 +1,14 @@
# SAML OmniAuth Provider
-GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as Microsoft ADFS to authenticate users.
+GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows
+GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as
+Microsoft ADFS to authenticate users.
-First configure SAML 2.0 support in GitLab, then register the GitLab application in your SAML IdP:
+First configure SAML 2.0 support in GitLab, then register the GitLab application
+in your SAML IdP:
-1. Make sure GitLab is configured with HTTPS. See [Using HTTPS](../install/installation.md#using-https) for instructions.
+1. Make sure GitLab is configured with HTTPS.
+ See [Using HTTPS](../install/installation.md#using-https) for instructions.
1. On your GitLab server, open the configuration file.
@@ -22,7 +26,40 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
+ for initial settings.
+
+1. To allow your users to use SAML to sign up without having to manually create
+ an account first, don't forget to add the following values to your configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+ gitlab_rails['omniauth_block_auto_created_users'] = false
+ ```
+
+ For installations from source:
+
+ ```yaml
+ allow_single_sign_on: ["saml"]
+ block_auto_created_users: false
+ ```
+
+1. You can also automatically link SAML users with existing GitLab users if their
+ email addresses match by adding the following setting:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_auto_link_saml_user'] = true
+ ```
+
+ For installations from source:
+
+ ```yaml
+ auto_link_saml_user: true
+ ```
1. Add the provider configuration:
@@ -31,15 +68,15 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
```ruby
gitlab_rails['omniauth_providers'] = [
{
- "name" => "saml",
- args: {
+ name: 'saml',
+ args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
},
- "label" => "Company Login" # optional label for SAML login button, defaults to "Saml"
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
]
```
@@ -47,37 +84,155 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
For installations from source:
```yaml
- - { name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
- } }
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
```
-1. Change the value for 'assertion_consumer_service_url' to match the HTTPS endpoint of GitLab (append 'users/auth/saml/callback' to the HTTPS URL of your GitLab installation to generate the correct value).
+1. Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
+ of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
+ installation to generate the correct value).
-1. Change the values of 'idp_cert_fingerprint', 'idp_sso_target_url', 'name_identifier_format' to match your IdP. Check [the omniauth-saml documentation](https://github.com/PracticallyGreen/omniauth-saml) for details on these options.
+1. Change the values of `idp_cert_fingerprint`, `idp_sso_target_url`,
+ `name_identifier_format` to match your IdP. Check
+ [the omniauth-saml documentation](https://github.com/omniauth/omniauth-saml)
+ for details on these options.
-1. Change the value of 'issuer' to a unique name, which will identify the application to the IdP.
+1. Change the value of `issuer` to a unique name, which will identify the application
+ to the IdP.
1. Restart GitLab for the changes to take effect.
-1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified in 'issuer'.
+1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified
+ in `issuer`.
-To ease configuration, most IdP accept a metadata URL for the application to provide configuration information to the IdP. To build the metadata URL for GitLab, append 'users/auth/saml/metadata' to the HTTPS URL of your GitLab installation, for instance:
+To ease configuration, most IdP accept a metadata URL for the application to provide
+configuration information to the IdP. To build the metadata URL for GitLab, append
+`users/auth/saml/metadata` to the HTTPS URL of your GitLab installation, for instance:
```
https://gitlab.example.com/users/auth/saml/metadata
```
-At a minimum the IdP *must* provide a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. GitLab will also use claims with name 'name', 'first_name', 'last_name' (see [the omniauth-saml gem](https://github.com/PracticallyGreen/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb) for supported claims).
-
-On the sign in page there should now be a SAML button below the regular sign in form. Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in.
+At a minimum the IdP *must* provide a claim containing the user's email address, using
+claim name `email` or `mail`. The email will be used to automatically generate the GitLab
+username. GitLab will also use claims with name `name`, `first_name`, `last_name`
+(see [the omniauth-saml gem](https://github.com/omniauth/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb)
+for supported claims).
+
+On the sign in page there should now be a SAML button below the regular sign in form.
+Click the icon to begin the authentication process. If everything goes well the user
+will be returned to GitLab and will be signed in.
+
+## Customization
+
+### `attribute_statements`
+
+>**Note:**
+This setting is only available on GitLab 8.6 and above.
+This setting should only be used to map attributes that are part of the
+OmniAuth info hash schema.
+
+`attribute_statements` is used to map Attribute Names in a SAMLResponse to entries
+in the OmniAuth [info hash](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema#schema-10-and-later).
+
+For example, if your SAMLResponse contains an Attribute called 'EmailAddress',
+specify `{ email: ['EmailAddress'] }` to map the Attribute to the
+corresponding key in the info hash. URI-named Attributes are also supported, e.g.
+`{ email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] }`.
+
+This setting allows you tell GitLab where to look for certain attributes required
+to create an account. Like mentioned above, if your IdP sends the user's email
+address as `EmailAddress` instead of `email`, let GitLab know by setting it on
+your configuration:
+
+```yaml
+args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+ attribute_statements: { email: ['EmailAddress'] }
+}
+```
+
+### `allowed_clock_drift`
+
+The clock of the Identity Provider may drift slightly ahead of your system clocks.
+To allow for a small amount of clock drift you can use `allowed_clock_drift` within
+your settings. Its value must be given in a number (and/or fraction) of seconds.
+The value given is added to the current time at which the response is validated.
+
+```yaml
+args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+ attribute_statements: { email: ['EmailAddress'] },
+ allowed_clock_drift: 1 # for one second clock drift
+}
+```
## Troubleshooting
-If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, this likely indicates that GitLab could not get the email address for the SAML user.
+### 500 error after login
+
+If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page,
+this likely indicates that GitLab could not get the email address for the SAML user.
+
+Make sure the IdP provides a claim containing the user's email address, using claim name
+`email` or `mail`.
+
+### Redirect back to login screen with no evident error
+
+If after signing in into your SAML server you are redirected back to the sign in page and
+no error is displayed, check your `production.log` file. It will most likely contain the
+message `Can't verify CSRF token authenticity`. This means that there is an error during
+the SAML request, but this error never reaches GitLab due to the CSRF check.
+
+To bypass this you can add `skip_before_action :verify_authenticity_token` to the
+`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab,
+where it can then be seen in the usual logs, or as a flash message in the login
+screen.
+
+### Invalid audience
+
+This error means that the IdP doesn't recognize GitLab as a valid sender and
+receiver of SAML requests. Make sure to add the GitLab callback URL to the approved
+audiences of the IdP server.
+
+### Missing claims
+
+The IdP server needs to pass certain information in order for GitLab to either
+create an account, or match the login information to an existing account. `email`
+is the minimum amount of information that needs to be passed. If the IdP server
+is not providing this information, all SAML requests will fail.
+
+Make sure this information is provided.
+
+### Key validation error, Digest mismatch or Fingerprint mismatch
+
+These errors all come from a similar place, the SAML certificate. SAML requests
+need to be validated using a fingerprint, a certificate or a validator.
+
+For this you need take the following into account:
+
+- If no certificate is provided in the settings, a fingerprint or fingerprint
+ validator needs to be provided and the response from the server must contain
+ a certificate (`<ds:KeyInfo><ds:X509Data><ds:X509Certificate>`)
+- If a certificate is provided in the settings, it is no longer necessary for
+ the request to contain one. In this case the fingerprint or fingerprint
+ validators are optional
-Make sure the IdP provides a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. \ No newline at end of file
+Make sure that one of the above described scenarios is valid, or the requests will
+fail with one of the mentioned errors. \ No newline at end of file
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index 6258e5f1030..a0be3dd4e5c 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -1,8 +1,8 @@
# Shibboleth OmniAuth Provider
-This documentation is for enabling shibboleth with gitlab-omnibus package.
+This documentation is for enabling shibboleth with omnibus-gitlab package.
-In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however I did not found way to easily configure Nginx that is bundled in gitlab-omnibus package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider.
+In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however I did not found way to easily configure Nginx that is bundled in omnibus-gitlab package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider.
To enable the Shibboleth OmniAuth provider you must:
diff --git a/doc/integration/slack.md b/doc/integration/slack.md
index 84f1d74c058..f6ba80f46d5 100644
--- a/doc/integration/slack.md
+++ b/doc/integration/slack.md
@@ -2,19 +2,13 @@
## On Slack
-To enable Slack integration you must create an Incoming WebHooks integration on Slack;
+To enable Slack integration you must create an Incoming WebHooks integration on Slack:
1. [Sign in to Slack](https://slack.com/signin)
-1. Select **Configure Integrations** from the dropdown next to your team name.
+1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
-1. Select the **All Services** tab
-
-1. Click **Add** next to Incoming Webhooks
-
-1. Pick Incoming WebHooks
-
-1. Choose the channel name you want to send notifications to
+1. Choose the channel name you want to send notifications to.
1. Click **Add Incoming WebHooks Integration**
- Optional step; You can change bot's name and avatar by clicking modifying the bot name or avatar under **Integration Settings**.
diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md
index 52ed4a22339..4769f26b259 100644
--- a/doc/integration/twitter.md
+++ b/doc/integration/twitter.md
@@ -14,7 +14,7 @@ To enable the Twitter OmniAuth provider you must register your application with
- Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback'
- Agree to the "Developer Agreement".
- ![Twitter App Details](twitter_app_details.png)
+ ![Twitter App Details](img/twitter_app_details.png)
1. Select "Create your Twitter application."
1. Select the "Settings" tab.
@@ -27,7 +27,7 @@ To enable the Twitter OmniAuth provider you must register your application with
1. You should now see an API key and API secret (see screenshot). Keep this page open as you continue configuration.
- ![Twitter app](twitter_app_api_keys.png)
+ ![Twitter app](img/twitter_app_api_keys.png)
1. On your GitLab server, open the configuration file.
@@ -76,4 +76,4 @@ To enable the Twitter OmniAuth provider you must register your application with
1. Restart GitLab for the changes to take effect.
-On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. \ No newline at end of file
+On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index f97c252fd7c..59803aea080 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -18,7 +18,7 @@ You accept and agree to the following terms and conditions for Your present and
6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [[]named here]".
+7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [insert_name_here]".
8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
diff --git a/doc/markdown/img/logo.png b/doc/markdown/img/logo.png
new file mode 100644
index 00000000000..7da5f23ed9b
--- /dev/null
+++ b/doc/markdown/img/logo.png
Binary files differ
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index bc8e7d155e7..e6eb1cf3819 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -29,6 +29,8 @@
## GitLab Flavored Markdown (GFM)
+_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
+
For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality.
You can use GFM in
@@ -88,6 +90,9 @@ GFM will autolink almost any URL you copy and paste into your text.
## Code and Syntax Highlighting
+_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
+list of supported languages visit the Rouge website._
+
Blocks of code are either fenced by lines with three back-ticks <code>```</code>, or are indented with four spaces. Only the fenced code blocks support syntax highlighting.
```no-highlight
@@ -204,6 +209,7 @@ GFM also recognizes certain cross-project references:
| `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
+| `namespace/project~"Some label"` | issues with given label |
## Task Lists
@@ -421,24 +427,24 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown
Here's our logo (hover to see the title text):
Inline-style:
- ![alt text](assets/logo-white.png)
+ ![alt text](img/logo.png)
Reference-style:
![alt text1][logo]
- [logo]: assets/logo-white.png
+ [logo]: img/logo.png
Here's our logo:
Inline-style:
-![alt text](/assets/logo-white.png)
+![alt text](img/logo.png)
Reference-style:
![alt text][logo]
-[logo]: /assets/logo-white.png
+[logo]: img/logo.png
## Blockquotes
@@ -585,3 +591,6 @@ By including colons in the header row, you can align the text within that column
- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
+
+[rouge]: http://rouge.jneen.net/ "Rouge website"
+[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md
new file mode 100644
index 00000000000..b856e7935a3
--- /dev/null
+++ b/doc/monitoring/performance/gitlab_configuration.md
@@ -0,0 +1,39 @@
+# GitLab Configuration
+
+GitLab Performance Monitoring is disabled by default. To enable it and change any of its
+settings, navigate to the Admin area in **Settings > Metrics**
+(`/admin/application_settings`).
+
+The minimum required settings you need to set are the InfluxDB host and port.
+Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the
+changes.
+
+---
+
+![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png)
+
+---
+
+Finally, a restart of all GitLab processes is required for the changes to take
+effect:
+
+```bash
+# For Omnibus installations
+sudo gitlab-ctl restart
+
+# For installations from source
+sudo service gitlab restart
+```
+
+## Pending Migrations
+
+When any migrations are pending, the metrics are disabled until the migrations
+have been performed.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [InfluxDB Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
diff --git a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
new file mode 100644
index 00000000000..14d82b6ac98
--- /dev/null
+++ b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
Binary files differ
diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md
new file mode 100644
index 00000000000..3a2b598b78f
--- /dev/null
+++ b/doc/monitoring/performance/influxdb_configuration.md
@@ -0,0 +1,192 @@
+# InfluxDB Configuration
+
+The default settings provided by [InfluxDB] are not sufficient for a high traffic
+GitLab environment. The settings discussed in this document are based on the
+settings GitLab uses for GitLab.com, depending on your own needs you may need to
+further adjust them.
+
+If you are intending to run InfluxDB on the same server as GitLab, make sure
+you have plenty of RAM since InfluxDB can use quite a bit depending on traffic.
+
+Unless you are going with a budget setup, it's advised to run it separately.
+
+## Requirements
+
+- InfluxDB 0.9.5 or newer
+- A fairly modern version of Linux
+- At least 4GB of RAM
+- At least 10GB of storage for InfluxDB data
+
+Note that the RAM and storage requirements can differ greatly depending on the
+amount of data received/stored. To limit the amount of stored data users can
+look into [InfluxDB Retention Policies][influxdb-retention].
+
+## Installation
+
+Installing InfluxDB is out of the scope of this document. Please refer to the
+[InfluxDB documentation].
+
+## InfluxDB Server Settings
+
+Since InfluxDB has many settings that users may wish to customize themselves
+(e.g. what port to run InfluxDB on), we'll only cover the essentials.
+
+The configuration file in question is usually located at
+`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file,
+InfluxDB needs to be restarted.
+
+### Storage Engine
+
+InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new
+storage engine is available, called [TSM Tree]. All users **must** use the new
+`tsm1` storage engine as this [will be the default engine][tsm1-commit] in
+upcoming InfluxDB releases.
+
+Make sure you have the following in your configuration file:
+
+```
+[data]
+ dir = "/var/lib/influxdb/data"
+ engine = "tsm1"
+```
+
+### Admin Panel
+
+Production environments should have the InfluxDB admin panel **disabled**. This
+feature can be disabled by adding the following to your InfluxDB configuration
+file:
+
+```
+[admin]
+ enabled = false
+```
+
+### HTTP
+
+HTTP is required when using the [InfluxDB CLI] or other tools such as Grafana,
+thus it should be enabled. When enabling make sure to _also_ enable
+authentication:
+
+```
+[http]
+ enabled = true
+ auth-enabled = true
+```
+
+_**Note:** Before you enable authentication, you might want to [create an
+admin user](#create-a-new-admin-user)._
+
+### UDP
+
+GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling
+UDP can be done using the following settings:
+
+```
+[[udp]]
+ enabled = true
+ bind-address = ":8089"
+ database = "gitlab"
+ batch-size = 1000
+ batch-pending = 5
+ batch-timeout = "1s"
+ read-buffer = 209715200
+```
+
+This does the following:
+
+1. Enable UDP and bind it to port 8089 for all addresses.
+2. Store any data received in the "gitlab" database.
+3. Define a batch of points to be 1000 points in size and allow a maximum of
+ 5 batches _or_ flush them automatically after 1 second.
+4. Define a UDP read buffer size of 200 MB.
+
+One of the most important settings here is the UDP read buffer size as if this
+value is set too low, packets will be dropped. You must also make sure the OS
+buffer size is set to the same value, the default value is almost never enough.
+
+To set the OS buffer size to 200 MB, on Linux you can run the following command:
+
+```bash
+sysctl -w net.core.rmem_max=209715200
+```
+
+To make this permanent, add the following to `/etc/sysctl.conf` and restart the
+server:
+
+```bash
+net.core.rmem_max=209715200
+```
+
+It is **very important** to make sure the buffer sizes are large enough to
+handle all data sent to InfluxDB as otherwise you _will_ lose data. The above
+buffer sizes are based on the traffic for GitLab.com. Depending on the amount of
+traffic, users may be able to use a smaller buffer size, but we highly recommend
+using _at least_ 100 MB.
+
+When enabling UDP, users should take care to not expose the port to the public,
+as doing so will allow anybody to write data into your InfluxDB database (as
+[InfluxDB's UDP protocol][udp] doesn't support authentication). We recommend either
+whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only
+allowing traffic from members of said VLAN.
+
+## Create a new admin user
+
+If you want to [enable authentication](#http), you might want to [create an
+admin user][influx-admin]:
+
+```
+influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES"
+```
+
+## Create the `gitlab` database
+
+Once you get InfluxDB up and running, you need to create a database for GitLab.
+Make sure you have changed the [storage engine](#storage-engine) to `tsm1`
+before creating a database.
+
+_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled
+[HTTP authentication](#http), remember to append the username (`-username <username>`)
+and password (`-password <password>`) you set earlier to the commands below._
+
+Run the following command to create a database named `gitlab`:
+
+```bash
+influx -execute 'CREATE DATABASE gitlab'
+```
+
+The name **must** be `gitlab`, do not use any other name.
+
+Next, make sure that the database was successfully created:
+
+```bash
+influx -execute 'SHOW DATABASES'
+```
+
+The output should be similar to:
+
+```
+name: databases
+---------------
+name
+_internal
+gitlab
+```
+
+That's it! Now your GitLab instance should send data to InfluxDB.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
+
+[influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management
+[influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/
+[influxdb cli]: https://docs.influxdata.com/influxdb/v0.9/tools/shell/
+[udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[tsm tree]: https://influxdata.com/blog/new-storage-engine-time-structured-merge-tree/
+[tsm1-commit]: https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d
+[influx-admin]: https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
new file mode 100644
index 00000000000..a5a8aebd2d1
--- /dev/null
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -0,0 +1,87 @@
+# InfluxDB Schema
+
+The following measurements are currently stored in InfluxDB:
+
+- `PROCESS_file_descriptors`
+- `PROCESS_gc_statistics`
+- `PROCESS_memory_usage`
+- `PROCESS_method_calls`
+- `PROCESS_object_counts`
+- `PROCESS_transactions`
+- `PROCESS_views`
+
+Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
+process type. In all series, any form of duration is stored in milliseconds.
+
+## PROCESS_file_descriptors
+
+This measurement contains the number of open file descriptors over time. The
+value field `value` contains the number of descriptors.
+
+## PROCESS_gc_statistics
+
+This measurement contains Ruby garbage collection statistics such as the amount
+of minor/major GC runs (relative to the last sampling interval), the time spent
+in garbage collection cycles, and all fields/values returned by `GC.stat`.
+
+## PROCESS_memory_usage
+
+This measurement contains the process' memory usage (in bytes) over time. The
+value field `value` contains the number of bytes.
+
+## PROCESS_method_calls
+
+This measurement contains the methods called during a transaction along with
+their duration, and a name of the transaction action that invoked the method (if
+available). The method call duration is stored in the value field `duration`,
+while the method name is stored in the tag `method`. The tag `action` contains
+the full name of the transaction action. Both the `method` and `action` fields
+are in the following format:
+
+```
+ClassName#method_name
+```
+
+For example, a method called by the `show` method in the `UsersController` class
+would have `action` set to `UsersController#show`.
+
+## PROCESS_object_counts
+
+This measurement is used to store retained Ruby objects (per class) and the
+amount of retained objects. The number of objects is stored in the `count` value
+field while the class name is stored in the `type` tag.
+
+## PROCESS_transactions
+
+This measurement is used to store basic transaction details such as the time it
+took to complete a transaction, how much time was spent in SQL queries, etc. The
+following value fields are available:
+
+| Value | Description |
+| ----- | ----------- |
+| `duration` | The total duration of the transaction |
+| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers |
+| `method_duration` | The total time spent in method calls |
+| `sql_duration` | The total time spent in SQL queries |
+| `view_duration` | The total time spent in views |
+
+## PROCESS_views
+
+This measurement is used to store view rendering timings for a transaction. The
+following value fields are available:
+
+| Value | Description |
+| ----- | ----------- |
+| `duration` | The rendering time of the view |
+| `view` | The path of the view, relative to the application's root directory |
+
+The `action` tag contains the action name of the transaction that rendered the
+view.
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Configuration](influxdb_configuration.md)
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
new file mode 100644
index 00000000000..f2460d31302
--- /dev/null
+++ b/doc/monitoring/performance/introduction.md
@@ -0,0 +1,64 @@
+# GitLab Performance Monitoring
+
+GitLab comes with its own application performance measuring system as of GitLab
+8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
+Community and Enterprise editions.
+
+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 Schema](influxdb_schema.md)
+
+## Introduction to GitLab Performance Monitoring
+
+GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
+including (but not limited to):
+
+- The time it took to complete a transaction (a web request or Sidekiq job).
+- The time spent in running SQL queries and rendering HAML views.
+- The time spent executing (instrumented) Ruby methods.
+- Ruby object allocations, and retained objects in particular.
+- System statistics such as the process' memory usage and open file descriptors.
+- Ruby garbage collection statistics.
+
+Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored
+data can be visualized using [Grafana][grafana] or any other application that
+supports reading data from InfluxDB. Alternatively data can be queried using the
+InfluxDB CLI.
+
+## Metric Types
+
+Two types of metrics are collected:
+
+1. Transaction specific metrics.
+1. Sampled metrics, collected at a certain interval in a separate thread.
+
+### Transaction Metrics
+
+Transaction metrics are metrics that can be associated with a single
+transaction. This includes statistics such as the transaction duration, timings
+of any executed SQL queries, time spent rendering HAML views, etc. These metrics
+are collected for every Rack request and Sidekiq job processed.
+
+### Sampled Metrics
+
+Sampled metrics are metrics that can't be associated with a single transaction.
+Examples include garbage collection statistics and retained Ruby objects. These
+metrics are collected at a regular interval. This interval is made up out of two
+parts:
+
+1. A user defined interval.
+1. A randomly generated offset added on top of the interval, the same offset
+ can't be used twice in a row.
+
+The actual interval can be anywhere between a half of the defined interval and a
+half above the interval. For example, for a user defined interval of 15 seconds
+the actual interval can be anywhere between 7.5 and 22.5. The interval is
+re-generated for every sampling run instead of being generated once and re-used
+for the duration of the process' lifetime.
+
+[influxdb]: https://influxdata.com/time-series-platform/influxdb/
+[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/
+[grafana]: http://grafana.org/
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 1be78ac1823..3d375e47c8e 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -6,7 +6,7 @@ If a user is both in a project group and in the project itself, the highest perm
If a user is a GitLab administrator they receive all permissions.
-On public projects the Guest role is not enforced.
+On public and internal projects the Guest role is not enforced.
All users will be able to create issues, leave comments, and pull or download the project code.
To add or import a user, you can follow the [project users and members
@@ -18,11 +18,15 @@ documentation](../workflow/add-user/add-user.md).
|---------------------------------------|---------|------------|-------------|----------|--------|
| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
+| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Pull project code | | ✓ | ✓ | ✓ | ✓ |
| Download project | | ✓ | ✓ | ✓ | ✓ |
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
| Manage labels | | ✓ | ✓ | ✓ | ✓ |
+| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| Manage merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
@@ -31,6 +35,8 @@ documentation](../workflow/add-user/add-user.md).
| Remove non-protected branches | | | ✓ | ✓ | ✓ |
| Add tags | | | ✓ | ✓ | ✓ |
| Write a wiki | | | ✓ | ✓ | ✓ |
+| Cancel and retry builds | | | ✓ | ✓ | ✓ |
+| Create or update commit status | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
@@ -40,12 +46,17 @@ documentation](../workflow/add-user/add-user.md).
| Edit project | | | | ✓ | ✓ |
| Add deploy keys to project | | | | ✓ | ✓ |
| Configure project hooks | | | | ✓ | ✓ |
+| Manage runners | | | | ✓ | ✓ |
+| Manage build triggers | | | | ✓ | ✓ |
+| Manage variables | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
| Force push to protected branches | | | | | |
| Remove protected branches | | | | | |
+[^1]: If **Allow guest to access builds** is enabled in CI settings
+
## Group
In order for a group to appear as public and be browsable, it must contain at
@@ -60,3 +71,24 @@ Any user can remove themselves from a group, unless they are the last Owner of t
| Create project in group | | | | ✓ | ✓ |
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
+
+## External Users
+
+In cases where it is desired that a user has access only to some internal or
+private projects, there is the option of creating **External Users**. This
+feature may be useful when for example a contractor is working on a given
+project and should only have access to that project.
+
+External users can only access projects to which they are explicitly granted
+access, thus hiding all other internal or private ones from them. Access can be
+granted by adding the user as member to the project or group.
+
+They will, like usual users, receive a role in the project or group with all
+the abilities that are mentioned in the table above. They cannot however create
+groups or projects, and they have the same access as logged out users in all
+other cases.
+
+An administrator can flag a user as external [through the API](../api/users.md)
+or by checking the checkbox on the admin panel. As an administrator, navigate
+to **Admin > Users** to create a new user or edit an existing one. There, you
+will find the option to flag the user as external.
diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md
index f17bbe8f2aa..073b8797508 100644
--- a/doc/profile/preferences.md
+++ b/doc/profile/preferences.md
@@ -12,6 +12,9 @@ The default is **Charcoal**.
## Syntax highlighting theme
+_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a
+list of supported languages visit the rouge website._
+
Changing this setting allows the user to customize the theme used when viewing
syntax highlighted code on the site.
@@ -36,3 +39,5 @@ The default is **Your Projects**.
It allows user to choose what content he or she want to see on project page.
The default is **Readme**.
+
+[rouge]: http://rouge.jneen.net/ "Rouge website"
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
new file mode 100644
index 00000000000..af0b1a287c7
--- /dev/null
+++ b/doc/project_services/builds_emails.md
@@ -0,0 +1,16 @@
+## Enabling build emails
+
+To receive e-mail notifications about the result status of your builds, visit
+your project's **Settings > Services > Builds emails** and activate the service.
+
+In the _Recipients_ area, provide a list of e-mails separated by comma.
+
+Check the _Add pusher_ checkbox if you want the committer to also receive
+e-mail notifications about each build's status.
+
+If you enable the _Notify only broken builds_ option, e-mail notifications will
+be sent only for failed builds.
+
+---
+
+![Builds emails service settings](img/builds_emails_service.png)
diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png
new file mode 100644
index 00000000000..e604dd73ffa
--- /dev/null
+++ b/doc/project_services/img/builds_emails_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png
new file mode 100644
index 00000000000..85e54861b3e
--- /dev/null
+++ b/doc/project_services/img/jira_add_gitlab_commit_message.png
Binary files differ
diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png
new file mode 100644
index 00000000000..e4576433889
--- /dev/null
+++ b/doc/project_services/img/jira_add_user_to_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png
new file mode 100644
index 00000000000..edaa1326058
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png
new file mode 100644
index 00000000000..9e518ad7843
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_group_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png
new file mode 100644
index 00000000000..57e433dd818
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_user.png
Binary files differ
diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png
new file mode 100644
index 00000000000..47716ca6d0e
--- /dev/null
+++ b/doc/project_services/img/jira_group_access.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png
new file mode 100644
index 00000000000..cabec1ae137
--- /dev/null
+++ b/doc/project_services/img/jira_issue_closed.png
Binary files differ
diff --git a/doc/integration/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png
index 15739a22dc7..15739a22dc7 100644
--- a/doc/integration/jira_issue_reference.png
+++ b/doc/project_services/img/jira_issue_reference.png
Binary files differ
diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png
new file mode 100644
index 00000000000..28e17be3a84
--- /dev/null
+++ b/doc/project_services/img/jira_issues_workflow.png
Binary files differ
diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png
new file mode 100644
index 00000000000..1e78daf105f
--- /dev/null
+++ b/doc/project_services/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/integration/jira_project_name.png b/doc/project_services/img/jira_project_name.png
index 5986fdb63fb..5986fdb63fb 100644
--- a/doc/integration/jira_project_name.png
+++ b/doc/project_services/img/jira_project_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png
new file mode 100644
index 00000000000..0149181dc86
--- /dev/null
+++ b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png
Binary files differ
diff --git a/doc/integration/jira_service.png b/doc/project_services/img/jira_service.png
index 1f6628c4371..1f6628c4371 100644
--- a/doc/integration/jira_service.png
+++ b/doc/project_services/img/jira_service.png
Binary files differ
diff --git a/doc/integration/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png
index 67dfc6144c4..67dfc6144c4 100644
--- a/doc/integration/jira_service_close_issue.png
+++ b/doc/project_services/img/jira_service_close_issue.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png
new file mode 100644
index 00000000000..2b37eda3520
--- /dev/null
+++ b/doc/project_services/img/jira_service_page.png
Binary files differ
diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png
new file mode 100644
index 00000000000..e935d9362aa
--- /dev/null
+++ b/doc/project_services/img/jira_submit_gitlab_merge_request.png
Binary files differ
diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png
new file mode 100644
index 00000000000..2745916972c
--- /dev/null
+++ b/doc/project_services/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/integration/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png
index 8635a32eb68..8635a32eb68 100644
--- a/doc/integration/jira_workflow_screenshot.png
+++ b/doc/project_services/img/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/project_services/img/redmine_configuration.png b/doc/project_services/img/redmine_configuration.png
new file mode 100644
index 00000000000..d14e526ad33
--- /dev/null
+++ b/doc/project_services/img/redmine_configuration.png
Binary files differ
diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/project_services/img/services_templates_redmine_example.png
new file mode 100644
index 00000000000..384d057fc8e
--- /dev/null
+++ b/doc/project_services/img/services_templates_redmine_example.png
Binary files differ
diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md
new file mode 100644
index 00000000000..27170c1eb19
--- /dev/null
+++ b/doc/project_services/jira.md
@@ -0,0 +1,234 @@
+# GitLab JIRA integration
+
+_**Note:**
+Full JIRA integration was previously exclusive to GitLab Enterprise Edition.
+With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce]
+to GitLab Community Edition as well._
+
+---
+
+GitLab can be configured to interact with [JIRA Core] either using an
+on-premises instance or the SaaS solution that Atlassian offers. Configuration
+happens via username and password on a per-project basis. Connecting to a JIRA
+server via CAS is not possible.
+
+Each project can be configured to connect to a different JIRA instance or, in
+case you have a single JIRA instance, you can pre-fill the JIRA service
+settings page in GitLab with a default template. To configure the JIRA template,
+see the [Services Templates documentation][services-templates].
+
+Once the GitLab project is connected to JIRA, you can reference and close the
+issues in JIRA directly from GitLab's merge requests.
+
+## Configuration
+
+The configuration consists of two parts:
+
+- [JIRA configuration](#configuring-jira)
+- [GitLab configuration](#configuring-gitlab)
+
+### Configuring JIRA
+
+First things first, we need to create a user in JIRA which will have access to
+all projects that need to integrate with GitLab.
+
+We have split this stage in steps so it is easier to follow.
+
+---
+
+1. Login to your JIRA instance as an administrator and under **Administration**
+ go to **User Management** to create a new user.
+
+ ![JIRA user management link](img/jira_user_management_link.png)
+
+ ---
+
+1. The next step is to create a new user (e.g., `gitlab`) who has write access
+ to projects in JIRA. Enter the user's name and a _valid_ e-mail address
+ since JIRA sends a verification e-mail to set-up the password.
+ _**Note:** JIRA creates the username automatically by using the e-mail
+ prefix. You can change it later if you want._
+
+ ![JIRA create new user](img/jira_create_new_user.png)
+
+ ---
+
+1. Now, let's create a `gitlab-developers` group which will have write access
+ to projects in JIRA. Go to the **Groups** tab and select **Create group**.
+
+ ![JIRA create new user](img/jira_create_new_group.png)
+
+ ---
+
+ Give it an optional description and hit **Create group**.
+
+ ![JIRA create new group](img/jira_create_new_group_name.png)
+
+ ---
+
+1. Give the newly-created group write access by going to
+ **Application access > View configuration** and adding the `gitlab-developers`
+ group to JIRA Core.
+
+ ![JIRA group access](img/jira_group_access.png)
+
+ ---
+
+1. Add the `gitlab` user to the `gitlab-developers` group by going to
+ **Users > GitLab user > Add group** and selecting the `gitlab-developers`
+ group from the dropdown menu. Notice that the group says _Access_ which is
+ what we aim for.
+
+ ![JIRA add user to group](img/jira_add_user_to_group.png)
+
+---
+
+The JIRA configuration is over. Write down the new JIRA username and its
+password as they will be needed when configuring GitLab in the next section.
+
+### Configuring GitLab
+
+_**Note:** The currently supported JIRA versions are v6.x and v7.x. and GitLab
+7.8 or higher is required._
+
+---
+
+Assuming you [have already configured JIRA](#configuring-jira), now it's time
+to configure GitLab.
+
+JIRA configuration in GitLab is done via a project's
+[**Services**](../project_services/project_services.md).
+
+To enable JIRA integration in a project, navigate to the project's
+**Settings > Services > JIRA**.
+
+Fill in the required details on the page, as described in the table below.
+
+| Setting | Description |
+| ------- | ----------- |
+| `Description` | A name for the issue tracker (to differentiate between instances, for example). |
+| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. |
+| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. |
+| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` |
+| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. |
+| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). |
+| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
+| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot](img/jira_issues_workflow.png)). By default, this ID is set to `2` |
+
+After saving the configuration, your GitLab project will be able to interact
+with the linked JIRA project.
+
+![JIRA service page](img/jira_service_page.png)
+
+---
+
+## JIRA issues
+
+By now you should have [configured JIRA](#configuring-jira) and enabled the
+[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly
+you should be able to reference and close JIRA issues by just mentioning their
+ID in GitLab commits and merge requests.
+
+### Referencing JIRA Issues
+
+If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link
+which points back to JIRA is created.
+
+The same works for comments in merge requests as well.
+
+![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png)
+
+---
+
+The mentioning action is two-fold, so a comment with a JIRA issue in GitLab
+will automatically add a comment in that particular JIRA issue with the link
+back to GitLab.
+
+
+![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png)
+
+---
+
+The comment on the JIRA issue is of the form:
+
+> USER mentioned this issue in LINK_TO_THE_MENTION
+
+Where:
+
+| Format | Description |
+| ------ | ----------- |
+| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. |
+| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. |
+
+### Closing JIRA issues
+
+JIRA issues can be closed directly from GitLab by using trigger words in
+commits and merge requests. When a commit which contains the trigger word
+followed by the JIRA issue ID in the commit message is pushed, GitLab will
+add a comment in the mentioned JIRA issue and immediately close it (provided
+the transition ID was set up correctly).
+
+There are currently three trigger words, and you can use either one to achieve
+the same goal:
+
+- `Resolves GITLAB-1`
+- `Closes GITLAB-1`
+- `Fixes GITLAB-1`
+
+where `GITLAB-1` the issue ID of the JIRA project.
+
+### JIRA issue closing example
+
+Let's say for example that we submitted a bug fix and created a merge request
+in GitLab. The workflow would be something like this:
+
+1. Create a new branch
+1. Fix the bug
+1. Commit the changes and push branch to GitLab
+1. Open a new merge request and reference the JIRA issue including one of the
+ trigger words, e.g.: `Fixes GITLAB-1`, in the description
+1. Submit the merge request
+1. Ask someone to review
+1. Merge the merge request
+1. The JIRA issue is automatically closed
+
+---
+
+In the following screenshot you can see what the link references to the JIRA
+issue look like.
+
+![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png)
+
+---
+
+Once this merge request is merged, the JIRA issue will be automatically closed
+with a link to the commit that resolved the issue.
+
+![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png)
+
+---
+
+You can see from the above image that there are four references to GitLab:
+
+- The first is from a comment in a specific commit
+- The second is from the JIRA issue reference in the merge request description
+- The third is from the actual commit that solved the issue
+- And the fourth is from the commit that the merge request created
+
+[services-templates]: ../project_services/services_templates.md "Services templates documentation"
+[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website"
+[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service"
+[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post"
+
+## Troubleshooting
+
+### GitLab is unable to comment on a ticket
+
+Make sure that the user you set up for GitLab to communicate with JIRA has the
+correct access permission to post comments on a ticket and to also transition the
+ticket, if you'd like GitLab to also take care of closing them.
+
+### GitLab is unable to close a ticket
+
+Make sure the the `Transition ID` you set within the JIRA settings matches the
+one your project needs to close a ticket.
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 03937d20728..3fea2cff0b9 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -1,20 +1,37 @@
# Project Services
-
-__Project integrations with external services for continuous integration and more.__
+
+Project services allow you to integrate GitLab with other applications. Below
+is list of the currently supported ones. Click on the service links to see
+further configuration instructions and details. Contributions are welcome.
## Services
-- Assembla
-- [Atlassian Bamboo CI](bamboo.md) An Atlassian product for continuous integration.
-- Build box
-- Campfire
-- Emails on push
-- Flowdock
-- Gemnasium
-- GitLab CI
-- [HipChat](hipchat.md) An Atlassian product for private group chat and instant messaging.
-- [Irker](irker.md) An IRC gateway to receive messages on repository updates.
-- Pivotal Tracker
-- Pushover
-- Slack
-- TeamCity
+| Service | Description |
+| ------- | ----------- |
+| Asana | Asana - Teamwork without email |
+| Assembla | Project Management Software (Source Commits Endpoint) |
+| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
+| Buildkite | Continuous integration and deployments |
+| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients |
+| Campfire | Simple web-based real-time group chat |
+| Custom Issue Tracker | Custom issue tracker |
+| Drone CI | Continuous Integration platform built on Docker, written in Go |
+| Emails on push | Email the commits and diff of each push to a list of recipients |
+| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
+| Flowdock | Flowdock is a collaboration web app for technical teams |
+| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
+| [HipChat](hipchat.md) | Private group chat and IM |
+| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
+| [JIRA](jira.md) | JIRA issue tracker |
+| JetBrains TeamCity CI | A continuous integration and build server |
+| PivotalTracker | Project Management Software (Source Commits Endpoint) |
+| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
+| [Redmine](redmine.md) | Redmine issue tracker |
+| Slack | A team communication tool for the 21st century |
+
+## Services Templates
+
+Services templates is a way to set some predefined values in the Service of
+your liking which will then be pre-filled on each project's Service.
+
+Read more about [Services Templates in this document](services_templates.md).
diff --git a/doc/project_services/redmine.md b/doc/project_services/redmine.md
new file mode 100644
index 00000000000..b9830ea7c38
--- /dev/null
+++ b/doc/project_services/redmine.md
@@ -0,0 +1,21 @@
+# Redmine Service
+
+Go to your project's **Settings > Services > Redmine** and fill in the required
+details as described in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
+| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project |
+
+Once you have configured and enabled Redmine:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Redmine issue index
+- clicking **New issue** on the project dashboard creates a new Redmine issue
+
+As an example, below is a configuration for a project named gitlab-ci.
+
+![Redmine configuration](img/redmine_configuration.png)
diff --git a/doc/project_services/services_templates.md b/doc/project_services/services_templates.md
new file mode 100644
index 00000000000..be6d13b6d2b
--- /dev/null
+++ b/doc/project_services/services_templates.md
@@ -0,0 +1,25 @@
+# Services Templates
+
+A GitLab administrator can add a service template that sets a default for each
+project. This makes it much easier to configure individual projects.
+
+After the template is created, the template details will be pre-filled on a
+project's Service page.
+
+## Enable a Service template
+
+In GitLab's Admin area, navigate to **Service Templates** and choose the
+service template you wish to create.
+
+For example, in the image below you can see Redmine.
+
+![Redmine service template](img/services_templates_redmine_example.png)
+
+---
+
+**NOTE:** For each project, you will still need to configure the issue tracking
+URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
+by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
+the project settings, and GitLab would automatically update the URL configured
+in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
+must be configured directly within the project's **Services** settings.
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index cc8a22cd003..6be954ad68b 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -6,6 +6,6 @@
- [Features](features.md)
- [Maintenance](maintenance.md) and self-checks
- [User management](user_management.md)
-- [Web hooks](web_hooks.md)
+- [Webhooks](web_hooks.md)
- [Import](import.md) of git repositories in bulk
- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index cdd6652b7b0..f6d1234ac4a 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -18,8 +18,6 @@ for two-factor authentication. If you restore a GitLab backup without
restoring the database encryption key, users who have two-factor
authentication enabled will lose access to your GitLab server.
-If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)*
-
```
# use this command if you've installed GitLab with the Omnibus package
sudo gitlab-rake gitlab:backup:create
diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md
index 5a8b94af9b4..2ebf7c48f4e 100644
--- a/doc/raketasks/web_hooks.md
+++ b/doc/raketasks/web_hooks.md
@@ -1,41 +1,41 @@
-# Web hooks
+# Webhooks
-## Add a web hook for **ALL** projects:
+## Add a webhook for **ALL** projects:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook"
# source installations
bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" RAILS_ENV=production
-## Add a web hook for projects in a given **NAMESPACE**:
+## Add a webhook for projects in a given **NAMESPACE**:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme
# source installations
bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production
-## Remove a web hook from **ALL** projects using:
+## Remove a webhook from **ALL** projects using:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook"
# source installations
bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" RAILS_ENV=production
-## Remove a web hook from projects in a given **NAMESPACE**:
+## Remove a webhook from projects in a given **NAMESPACE**:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme
# source installations
bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production
-## List **ALL** web hooks:
+## List **ALL** webhooks:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:list
# source installations
bundle exec rake gitlab:web_hook:list RAILS_ENV=production
-## List the web hooks from projects in a given **NAMESPACE**:
+## List the webhooks from projects in a given **NAMESPACE**:
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:list NAMESPACE=/
diff --git a/doc/release/patch.md b/doc/release/patch.md
index 3022e375aca..1c921439156 100644
--- a/doc/release/patch.md
+++ b/doc/release/patch.md
@@ -24,7 +24,7 @@ Use the following template:
- Picked into respective `stable` branches:
- [ ] Merge `x-y-stable` into `x-y-stable-ee`
- [ ] release-tools: `x.y.z`
-- gitlab-omnibus
+- omnibus-gitlab
- [ ] `x.y.z+ee.0`
- [ ] `x.y.z+ce.0`
- [ ] Deploy
diff --git a/doc/release/security.md b/doc/release/security.md
index b1a62b333e6..118c016ba4f 100644
--- a/doc/release/security.md
+++ b/doc/release/security.md
@@ -15,7 +15,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c
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](doc/release/patch.md), starting with "Create an issue on private GitLab development server"
+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
diff --git a/doc/security/README.md b/doc/security/README.md
index f34c792d005..4cd0fdd4094 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -2,9 +2,9 @@
- [Password length limits](password_length_limits.md)
- [Rack attack](rack_attack.md)
-- [Web Hooks and insecure internal web services](webhooks.md)
+- [Webhooks and insecure internal web services](webhooks.md)
- [Information exclusivity](information_exclusivity.md)
- [Reset your root password](reset_root_password.md)
- [User File Uploads](user_file_uploads.md)
- [How we manage the CRIME vulnerability](crime_vulnerability.md)
-- [Enforce Two-Factor authentication](two_factor_authentication.md)
+- [Enforce Two-factor authentication](two_factor_authentication.md)
diff --git a/doc/security/img/two_factor_authentication_settings.png b/doc/security/img/two_factor_authentication_settings.png
new file mode 100644
index 00000000000..aa51ce030bb
--- /dev/null
+++ b/doc/security/img/two_factor_authentication_settings.png
Binary files differ
diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md
index 4e25a1fdc3f..c8499380c18 100644
--- a/doc/security/two_factor_authentication.md
+++ b/doc/security/two_factor_authentication.md
@@ -6,7 +6,7 @@ password to login, they'll be prompted for a code generated by an application on
their phone.
You can read more about it here:
-[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md)
+[Two-factor Authentication (2FA)](../profile/two_factor_authentication.md)
## Enabling 2FA
@@ -20,7 +20,13 @@ In the Admin area under **Settings** (`/admin/application_settings`), look for
the "Sign-in Restrictions" area, where you can configure both.
If you want 2FA enforcement to take effect on next login, change the grace
-period to `0`
+period to `0`.
+
+---
+
+![Two factor authentication admin settings](img/two_factor_authentication_settings.png)
+
+---
## Disabling 2FA for everyone
@@ -28,11 +34,12 @@ There may be some special situations where you want to disable 2FA for everyone
even when forced 2FA is disabled. There is a rake task for that:
```
-# use this command if you've installed GitLab with the Omnibus package
+# Omnibus installations
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
-# if you've installed GitLab from source
+# Installations from source
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
-**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.**
+**IMPORTANT: this is a permanent and irreversible action. Users will have to
+ reactivate 2FA from scratch if they want to use it again.**
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index 1e9d33e87c3..bb46aebf4b5 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -1,13 +1,13 @@
-# Web Hooks and insecure internal web services
+# Webhooks and insecure internal web services
-If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Web Hooks.
+If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
-With [Web Hooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
+With [Webhooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
-Things get hairy, however, when a Web Hook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the web hook is triggered and the POST request is sent.
+Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
-Because Web Hook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world.
+Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world.
-If a web service does not require authentication, Web Hooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
+If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. \ No newline at end of file
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index fe5b45dd432..a1198e5878f 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -5,11 +5,17 @@
An SSH key allows you to establish a secure connection between your
computer and GitLab. Before generating an SSH key in your shell, check if your system
already has one by running the following command:
+
+**Windows Command Line:**
+```bash
+type %userprofile%\.ssh\id_rsa.pub
+```
+**GNU/Linux/Mac/PowerShell:**
```bash
cat ~/.ssh/id_rsa.pub
```
-If you see a long string starting with `ssh-rsa` or `ssh-dsa`, you can skip the `ssh-keygen` step.
+If you see a long string starting with `ssh-rsa`, you can skip the `ssh-keygen` step.
Note: It is a best practice to use a password for an SSH key, but it is not
required and you can skip creating a password by pressing enter. Note that
@@ -20,24 +26,36 @@ To generate a new SSH key, use the following command:
ssh-keygen -t rsa -C "$your_email"
```
This command will prompt you for a location and filename to store the key
-pair and for a password. When prompted for the location and filename, you
-can press enter to use the default.
+pair and for a password. When prompted for the location and filename, just
+press enter to use the default. If you use a different name, the key will not
+be used automatically.
Use the command below to show your public key:
+
+**Windows Command Line:**
+```bash
+type %userprofile%\.ssh\id_rsa.pub
+```
+**GNU/Linux/Mac/PowerShell:**
```bash
cat ~/.ssh/id_rsa.pub
```
Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your
-user profile. Please copy the complete key starting with `ssh-` and ending
+user profile. Please copy the complete key starting with `ssh-rsa` and ending
with your username and host.
-To copy your public key to the clipboard, use code below. Depending on your
+To copy your public key to the clipboard, use the code below. Depending on your
OS you'll need to use a different command:
-**Windows:**
+**Windows Command Line:**
+```bash
+type %userprofile%\.ssh\id_rsa.pub | clip
+```
+
+**Windows PowerShell:**
```bash
-clip < ~/.ssh/id_rsa.pub
+cat ~/.ssh/id_rsa.pub | clip
```
**Mac:**
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 5cb05b13b3e..612376e3a49 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -1,6 +1,6 @@
# System hooks
-Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
+Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
System hooks can be used, e.g. for logging or changing information in a LDAP server.
@@ -17,6 +17,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_create",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -33,6 +34,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_destroy",
"name": "Underscore",
"owner_email": "johnsmith@gmail.com",
@@ -44,11 +46,48 @@ X-Gitlab-Event: System Hook
}
```
+**Project renamed:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_rename",
+ "name": "Underscore",
+ "path": "underscore",
+ "path_with_namespace": "jsmith/underscore",
+ "project_id": 73,
+ "owner_name": "John Smith",
+ "owner_email": "johnsmith@gmail.com",
+ "project_visibility": "internal",
+ "old_path_with_namespace": "jsmith/overscore",
+}
+```
+
+**Project transferred:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_transfer",
+ "name": "Underscore",
+ "path": "underscore",
+ "path_with_namespace": "scores/underscore",
+ "project_id": 73,
+ "owner_name": "John Smith",
+ "owner_email": "johnsmith@gmail.com",
+ "project_visibility": "internal",
+ "old_path_with_namespace": "jsmith/overscore",
+}
+```
+
**New Team Member:**
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_team",
"project_access": "Master",
"project_id": 74,
@@ -57,6 +96,7 @@ X-Gitlab-Event: System Hook
"project_path_with_namespace": "jsmith/storecloud",
"user_email": "johnsmith@gmail.com",
"user_name": "John Smith",
+ "user_username": "johnsmith",
"user_id": 41,
"project_visibility": "private",
}
@@ -67,6 +107,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_team",
"project_access": "Master",
"project_id": 74,
@@ -75,6 +116,7 @@ X-Gitlab-Event: System Hook
"project_path_with_namespace": "jsmith/storecloud",
"user_email": "johnsmith@gmail.com",
"user_name": "John Smith",
+ "user_username": "johnsmith",
"user_id": 41,
"project_visibility": "private",
}
@@ -85,9 +127,11 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:44:07Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"email": "js@gitlabhq.com",
"event_name": "user_create",
"name": "John Smith",
+ "username": "js",
"user_id": 41
}
```
@@ -97,9 +141,11 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:44:07Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"email": "js@gitlabhq.com",
"event_name": "user_destroy",
"name": "John Smith",
+ "username": "js",
"user_id": 41
}
```
@@ -110,6 +156,7 @@ X-Gitlab-Event: System Hook
{
"event_name": "key_create",
"created_at": "2014-08-18 18:45:16 UTC",
+ "updated_at": "2012-07-21T07:38:22Z",
"username": "root",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost",
"id": 4
@@ -122,6 +169,7 @@ X-Gitlab-Event: System Hook
{
"event_name": "key_destroy",
"created_at": "2014-08-18 18:45:16 UTC",
+ "updated_at": "2012-07-21T07:38:22Z",
"username": "root",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost",
"id": 4
@@ -133,6 +181,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_create",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -147,6 +196,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_destroy",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -161,6 +211,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_group",
"group_access": "Master",
"group_id": 78,
@@ -168,6 +219,7 @@ X-Gitlab-Event: System Hook
"group_path": "storecloud",
"user_email": "johnsmith@gmail.com",
"user_name": "John Smith",
+ "user_username": "johnsmith",
"user_id": 41
}
```
@@ -176,6 +228,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_group",
"group_access": "Master",
"group_id": 78,
@@ -183,6 +236,7 @@ X-Gitlab-Event: System Hook
"group_path": "storecloud",
"user_email": "johnsmith@gmail.com",
"user_name": "John Smith",
+ "user_username": "johnsmith",
"user_id": 41
}
```
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index 4516a102084..c45fc9340ea 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -14,6 +14,12 @@ possible to edit the label text and color. The characters `?`, `&` and `,` are
no longer allowed however so those will be removed from your tags during the
database migrations for GitLab 7.2.
+## Stash changes
+
+If you [deleted the vendors folder during your original installation](https://github.com/gitlabhq/gitlabhq/issues/4883#issuecomment-31108431), [you will get an error](https://gitlab.com/gitlab-org/gitlab-ce/issues/1494) when you attempt to rebuild the assets in step 7. To avoid this, stash the changes in your GitLab working copy before starting:
+
+ git stash
+
## 0. Stop server
sudo service gitlab stop
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index e028975d4ee..9f5c6c4dc84 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -87,7 +87,7 @@ which should already be on your system from GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout 0.5.1
+sudo -u git -H git checkout 0.5.4
sudo -u git -H make
```
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
new file mode 100644
index 00000000000..269deec7a9c
--- /dev/null
+++ b/doc/update/8.3-to-8.4.md
@@ -0,0 +1,124 @@
+# From 8.3 to 8.4
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-4-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-4-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v2.6.10
+```
+
+### 5. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires [Go 1.5](https://golang.org/dl)
+which should already be on your system from GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout 0.6.2
+sudo -u git -H make
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+```
+
+### 7. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-3-stable:config/gitlab.yml.example origin/8-4-stable:config/gitlab.yml.example
+```
+
+#### Init script
+
+We updated the init script for GitLab in order to set a specific PATH for gitlab-workhorse.
+
+```
+cd /home/git/gitlab
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+### 8. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 9. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.3)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.2 to 8.3](8.2-to-8.3.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
new file mode 100644
index 00000000000..0a9cb5683e7
--- /dev/null
+++ b/doc/update/8.4-to-8.5.md
@@ -0,0 +1,145 @@
+# From 8.4 to 8.5
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-5-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-5-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v2.6.10
+```
+
+### 5. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout 0.6.4
+sudo -u git -H make
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+```
+
+### 7. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-4-stable:config/gitlab.yml.example origin/8-5-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-4-stable:lib/support/nginx/gitlab-ssl origin/8-5-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-4-stable:lib/support/nginx/gitlab origin/8-5-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-5-stable/lib/support/init.d/gitlab.default.example#L37
+
+#### Init script
+
+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. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 9. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.4)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.3 to 8.4](8.3-to-8.4.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
new file mode 100644
index 00000000000..024f6e8a433
--- /dev/null
+++ b/doc/update/8.5-to-8.6.md
@@ -0,0 +1,164 @@
+# From 8.5 to 8.6
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-6-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-6-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v2.6.11
+```
+
+### 5. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```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 make
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+```
+
+### 7. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-5-stable:config/gitlab.yml.example origin/8-6-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-5-stable:lib/support/nginx/gitlab-ssl origin/8-6-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-5-stable:lib/support/nginx/gitlab origin/8-6-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-6-stable/lib/support/init.d/gitlab.default.example#L37
+
+#### Init script
+
+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
+ sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.5)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.4 to 8.5](8.4-to-8.5.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/README.md b/doc/update/README.md
index 0472537eeb5..109d5de3fa2 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -14,3 +14,4 @@ Depending on the installation method and your GitLab version, there are multiple
## Miscellaneous
- [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.
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index c19ee49f9e0..f446ed0a35b 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -48,6 +48,7 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch
sudo -u git -H git checkout `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION`
+sudo -u git -H make
```
### 5. Install libs, migrations, etc.
@@ -61,7 +62,13 @@ sudo -u git -H bundle install --without development test mysql --deployment
# MySQL
sudo -u git -H bundle install --without development test postgres --deployment
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md
index fd0327686b1..5fa39ef1b0a 100644
--- a/doc/update/upgrader.md
+++ b/doc/update/upgrader.md
@@ -4,7 +4,7 @@
Although deprecated, if someone wants to make this script into a gem or otherwise improve it merge requests are welcome.
-*Make sure you view this [upgrade guide from the 'master' branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the 'master' branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md) for the most up to date instructions.*
GitLab Upgrader - a ruby script that allows you easily upgrade GitLab to latest minor version.
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 6420d65cf1b..afdf1a682e2 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -1,18 +1,25 @@
-# Web hooks
+# Webhooks
-Project web hooks allow you to trigger an URL if new code is pushed or a new issue is created.
+_**Note:**
+Starting from GitLab 8.5:_
-You can configure web hooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the web hook URL.
+- _the `repository` key is deprecated in favor of the `project` key_
+- _the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key_
+- _the `project.http_url` key is deprecated in favor of the `project.git_http_url` key_
-Web hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server.
+Project webhooks allow you to trigger an URL if new code is pushed or a new issue is created.
+
+You can configure webhooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the webhook URL.
+
+Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server.
## SSL Verification
-By default, the SSL certificate of the webhook endpoint is verified based on
-an internal list of Certificate Authorities,
+By default, the SSL certificate of the webhook endpoint is verified based on
+an internal list of Certificate Authorities,
which means the certificate cannot be self-signed.
-You can turn this off in the web hook settings in your GitLab projects.
+You can turn this off in the webhook settings in your GitLab projects.
![SSL Verification](ssl.png)
@@ -37,8 +44,25 @@ X-Gitlab-Event: Push Hook
"user_id": 4,
"user_name": "John Smith",
"user_email": "john@example.com",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 15,
- "repository": {
+ "project":{
+ "name":"Diaspora",
+ "description":"",
+ "web_url":"http://example.com/mike/diaspora",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "namespace":"Mike",
+ "visibility_level":0,
+ "path_with_namespace":"mike/diaspora",
+ "default_branch":"master",
+ "homepage":"http://example.com/mike/diaspora",
+ "url":"git@example.com:mike/diasporadiaspora.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",
"description": "",
@@ -56,7 +80,7 @@ X-Gitlab-Event: Push Hook
"author": {
"name": "Jordi Mallach",
"email": "jordi@softcatala.org"
- }
+ },
"added": ["CHANGELOG"],
"modified": ["app/controller/application.rb"],
"removed": []
@@ -76,7 +100,6 @@ X-Gitlab-Event: Push Hook
}
],
"total_commits_count": 4
-
}
```
@@ -101,8 +124,25 @@ X-Gitlab-Event: Tag Push Hook
"after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
"user_id": 1,
"user_name": "John Smith",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 1,
- "repository": {
+ "project":{
+ "name":"Example",
+ "description":"",
+ "web_url":"http://example.com/jsmith/example",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "namespace":"Jsmith",
+ "visibility_level":0,
+ "path_with_namespace":"jsmith/example",
+ "default_branch":"master",
+ "homepage":"http://example.com/jsmith/example",
+ "url":"git@example.com:jsmith/example.git",
+ "ssh_url":"git@example.com:jsmith/example.git",
+ "http_url":"http://example.com/jsmith/example.git"
+ },
+ "repository":{
"name": "jsmith",
"url": "ssh://git@example.com/jsmith/example.git",
"description": "",
@@ -136,7 +176,23 @@ X-Gitlab-Event: Issue Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
- "repository": {
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository":{
"name": "Gitlab Test",
"url": "http://example.com/gitlabhq/gitlab-test.git",
"description": "Aut reprehenderit ut est.",
@@ -158,6 +214,11 @@ X-Gitlab-Event: Issue Hook
"iid": 23,
"url": "http://example.com/diaspora/issues/23",
"action": "open"
+ },
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
}
}
```
@@ -193,9 +254,25 @@ X-Gitlab-Event: Note Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project_id": 5,
- "repository": {
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository":{
"name": "Gitlab Test",
- "url": "http://localhost/gitlab-org/gitlab-test.git",
+ "url": "http://example.com/gitlab-org/gitlab-test.git",
"description": "Aut reprehenderit ut est.",
"homepage": "http://example.com/gitlab-org/gitlab-test"
},
@@ -256,9 +333,25 @@ X-Gitlab-Event: Note Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project_id": 5,
- "repository": {
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
"name": "Gitlab Test",
- "url": "http://example.com/gitlab-org/gitlab-test.git",
+ "url": "http://localhost/gitlab-org/gitlab-test.git",
"description": "Aut reprehenderit ut est.",
"homepage": "http://example.com/gitlab-org/gitlab-test"
},
@@ -296,21 +389,37 @@ X-Gitlab-Event: Note Hook
"description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
"position": 0,
"locked_at": null,
- "source": {
- "name": "Gitlab Test",
- "ssh_url": "git@example.com:gitlab-org/gitlab-test.git",
- "http_url": "http://example.com/gitlab-org/gitlab-test.git",
- "web_url": "http://example.com/gitlab-org/gitlab-test",
- "namespace": "Gitlab Org",
- "visibility_level": 10
+ "source":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
},
"target": {
- "name": "Gitlab Test",
- "ssh_url": "git@example.com:gitlab-org/gitlab-test.git",
- "http_url": "http://example.com/gitlab-org/gitlab-test.git",
- "web_url": "http://example.com/gitlab-org/gitlab-test",
- "namespace": "Gitlab Org",
- "visibility_level": 10
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
},
"last_commit": {
"id": "562e173be03b8ff2efb05345d12df18815438a4b",
@@ -322,7 +431,12 @@ X-Gitlab-Event: Note Hook
"email": "john@example.com"
}
},
- "work_in_progress": false
+ "work_in_progress": false,
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
}
}
```
@@ -346,11 +460,27 @@ X-Gitlab-Event: Note Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project_id": 5,
- "repository": {
- "name": "Gitlab Test",
- "url": "http://example.com/gitlab-org/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlab-org/gitlab-test"
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name":"diaspora",
+ "url":"git@example.com:mike/diasporadiaspora.git",
+ "description":"",
+ "homepage":"http://example.com/mike/diaspora"
},
"object_attributes": {
"id": 1241,
@@ -388,7 +518,6 @@ X-Gitlab-Event: Note Hook
### Comment on code snippet
-
**Request header**:
```
@@ -397,7 +526,7 @@ X-Gitlab-Event: Note Hook
**Request body:**
-```
+```json
{
"object_kind": "note",
"user": {
@@ -406,11 +535,27 @@ X-Gitlab-Event: Note Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project_id": 5,
- "repository": {
- "name": "Gitlab Test",
- "url": "http://example.com/gitlab-org/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlab-org/gitlab-test"
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name":"Gitlab Test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "description":"Aut reprehenderit ut est.",
+ "homepage":"http://example.com/gitlab-org/gitlab-test"
},
"object_attributes": {
"id": 1245,
@@ -482,21 +627,37 @@ X-Gitlab-Event: Merge Request Hook
"target_project_id": 14,
"iid": 1,
"description": "",
- "source": {
- "name": "awesome_project",
- "ssh_url": "ssh://git@example.com/awesome_space/awesome_project.git",
- "http_url": "http://example.com/awesome_space/awesome_project.git",
- "web_url": "http://example.com/awesome_space/awesome_project",
- "visibility_level": 20,
- "namespace": "awesome_space"
+ "source":{
+ "name":"Awesome Project",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/awesome_space/awesome_project",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "git_http_url":"http://example.com/awesome_space/awesome_project.git",
+ "namespace":"Awesome Space",
+ "visibility_level":20,
+ "path_with_namespace":"awesome_space/awesome_project",
+ "default_branch":"master",
+ "homepage":"http://example.com/awesome_space/awesome_project",
+ "url":"http://example.com/awesome_space/awesome_project.git",
+ "ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "http_url":"http://example.com/awesome_space/awesome_project.git"
},
"target": {
- "name": "awesome_project",
- "ssh_url": "ssh://git@example.com/awesome_space/awesome_project.git",
- "http_url": "http://example.com/awesome_space/awesome_project.git",
- "web_url": "http://example.com/awesome_space/awesome_project",
- "visibility_level": 20,
- "namespace": "awesome_space"
+ "name":"Awesome Project",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/awesome_space/awesome_project",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "git_http_url":"http://example.com/awesome_space/awesome_project.git",
+ "namespace":"Awesome Space",
+ "visibility_level":20,
+ "path_with_namespace":"awesome_space/awesome_project",
+ "default_branch":"master",
+ "homepage":"http://example.com/awesome_space/awesome_project",
+ "url":"http://example.com/awesome_space/awesome_project.git",
+ "ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "http_url":"http://example.com/awesome_space/awesome_project.git"
},
"last_commit": {
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
@@ -510,7 +671,12 @@ X-Gitlab-Event: Merge Request Hook
},
"work_in_progress": false,
"url": "http://example.com/diaspora/merge_requests/1",
- "action": "open"
+ "action": "open",
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
}
}
```
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 3651b55f438..25893f948ea 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -6,17 +6,22 @@
- [GitLab Flow](gitlab_flow.md)
- [Groups](groups.md)
- [Keyboard shortcuts](shortcuts.md)
+- [File finder](file_finder.md)
- [Labels](labels.md)
- [Notification emails](notifications.md)
- [Project Features](project_features.md)
- [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md)
- [Protected branches](protected_branches.md)
+- [Sharing a project with a group](share_with_group.md)
+- [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
+- [Revert changes](revert_changes.md)
- ["Work In Progress" Merge Requests](wip_merge_requests.md)
- [Merge When Build Succeeds](merge_when_build_succeeds.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, BitBucket, etc](importing/README.md)
+- [Todos](todos.md)
diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md
index 8c9b4f72631..fffa0aba57f 100644
--- a/doc/workflow/add-user/add-user.md
+++ b/doc/workflow/add-user/add-user.md
@@ -1,25 +1,89 @@
# Project users
-You can manage the groups and users and their access levels in all of your projects. You can also personalize the access level you give each user, per project.
+You can manage the groups and users and their access levels in all of your
+projects. You can also personalize the access level you give each user,
+per-project.
-Here's how to add or import users to your projects.
-
-You should have 'master' or 'owner' permissions to add or import a new user
+You should have `master` or `owner` permissions to add or import a new user
to your project.
-To add or import a user, go to your project and click on "Members" on the left side of your screen:
+The first step to add or import a user, go to your project and click on
+**Members** on the left side of your screen.
+
+![Members](img/add_user_members_menu.png)
+
+---
+
+## Add a user
+
+Right next to **People**, start typing the name or username of the user you
+want to add.
+
+![Search for people](img/add_user_search_people.png)
+
+---
+
+Select the user and the [permission level](../../permissions/permissions.md)
+that you'd like to give the user. Note that you can select more than one user.
+
+![Give user permissions](img/add_user_give_permissions.png)
+
+---
+
+Once done, hit **Add users to project** and they will be immediately added to
+your project with the permissions you gave them above.
+
+![List members](img/add_user_list_members.png)
+
+---
+
+From there on, you can either remove an existing user or change their access
+level to the project.
+
+## Import users from another project
+
+You can import another project's users in your own project by hitting the
+**Import members** button on the upper right corner of the **Members** menu.
+
+In the dropdown menu, you can see only the projects you are Master on.
+
+![Import members from another project](img/add_user_import_members_from_another_project.png)
+
+---
+
+Select the one you want and hit **Import project members**. A flash message
+notifying you that the import was successful will appear, and the new members
+are now in the project's members list. Notice that the permissions that they
+had on the project you imported from are retained.
+
+![Members list of new members](img/add_user_imported_members.png)
+
+---
+
+## Invite people using their e-mail address
+
+If a user you want to give access to doesn't have an account on your GitLab
+instance, you can invite them just by typing their e-mail address in the
+user search field.
+
+![Invite user by mail](img/add_user_email_search.png)
+
+---
-![Members](images/members.png)
+As you can imagine, you can mix inviting multiple people and adding existing
+GitLab users to the project.
-Select "Add members" or "Import members" on the right side of your screen:
+![Invite user by mail ready to submit](img/add_user_email_ready.png)
-![Add or Import](images/add-members.png)
+---
-If you are adding a user, select the user and the [permission level](doc/permissions/permissions.md) that you'd like to
-give the user:
+Once done, hit **Add users to project** and watch that there is a new member
+with the e-mail address we used above. From there on, you can resend the
+invitation, change their access level or even delete them.
-![Add or Import](images/new-member.png)
+![Invite user members list](img/add_user_email_accept.png)
-If you are importing a user, follow the steps to select the project where you'd like to import the user from:
+---
-![Add or Import](images/select-project.png)
+Once the user accepts the invitation, they will be prompted to create a new
+GitLab account using the same e-mail address the invitation was sent to.
diff --git a/doc/workflow/add-user/images/add-members.png b/doc/workflow/add-user/images/add-members.png
deleted file mode 100644
index 2805c5764a5..00000000000
--- a/doc/workflow/add-user/images/add-members.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/add-user/images/new-member.png b/doc/workflow/add-user/images/new-member.png
deleted file mode 100644
index d500daea56e..00000000000
--- a/doc/workflow/add-user/images/new-member.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/add-user/images/select-project.png b/doc/workflow/add-user/images/select-project.png
deleted file mode 100644
index dd3844edff8..00000000000
--- a/doc/workflow/add-user/images/select-project.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/add-user/img/add_new_user_to_project_settings.png b/doc/workflow/add-user/img/add_new_user_to_project_settings.png
new file mode 100644
index 00000000000..3da18cdae53
--- /dev/null
+++ b/doc/workflow/add-user/img/add_new_user_to_project_settings.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png
new file mode 100644
index 00000000000..910affc9659
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_email_accept.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png
new file mode 100644
index 00000000000..5f02ce89b3e
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_email_ready.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png
new file mode 100644
index 00000000000..140979fbe13
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_email_search.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png
new file mode 100644
index 00000000000..8ef9156c8d5
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_give_permissions.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
new file mode 100644
index 00000000000..5770d5cf0c4
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png
new file mode 100644
index 00000000000..dea4b3f40ad
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_imported_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png
new file mode 100644
index 00000000000..7daa6ca7d9e
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_list_members.png
Binary files differ
diff --git a/doc/workflow/add-user/images/members.png b/doc/workflow/add-user/img/add_user_members_menu.png
index f1797b95f67..f1797b95f67 100644
--- a/doc/workflow/add-user/images/members.png
+++ b/doc/workflow/add-user/img/add_user_members_menu.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png
new file mode 100644
index 00000000000..5ac10ce80d4
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_search_people.png
Binary files differ
diff --git a/doc/workflow/file_finder.md b/doc/workflow/file_finder.md
new file mode 100644
index 00000000000..b69ae663272
--- /dev/null
+++ b/doc/workflow/file_finder.md
@@ -0,0 +1,46 @@
+# File finder
+
+_**Note:** This feature was [introduced][gh-9889] in GitLab 8.4._
+
+---
+
+The file finder feature allows you to quickly shortcut your way when you are
+searching for a file in a repository using the GitLab UI.
+
+You can find the **Find File** button when in the **Files** section of a
+project.
+
+![Find file button](img/file_finder_find_button.png)
+
+---
+
+For those who prefer to keep their fingers on the keyboard, there is a
+[shortcut button](shortcuts.md) as well, which you can invoke from _anywhere_
+in a project.
+
+Press `t` to launch the File search function when in **Issues**,
+**Merge requests**, **Milestones**, even the project's settings.
+
+Start typing what you are searching for and watch the magic happen. With the
+up/down arrows, you go up and down the results, with `Esc` you close the search
+and go back to **Files**.
+
+## How it works
+
+The File finder feature is powered by the [Fuzzy filter] library.
+
+It implements a fuzzy search with highlight, and tries to provide intuitive
+results by recognizing patterns that people use while searching.
+
+For example, consider the [GitLab CE repository][ce] and that we want to open
+the `app/controllers/admin/deploy_keys_controller.rb` file.
+
+Using fuzzy search, we start by typing letters that get us closer to the file.
+
+**Protip:** To narrow down your search, include `/` in your search terms.
+
+![Find file button](img/file_finder_find_file.png)
+
+[gh-9889]: https://github.com/gitlabhq/gitlabhq/pull/9889 "File finder pull request"
+[fuzzy filter]: https://github.com/jeancroy/fuzzaldrin-plus "fuzzaldrin-plus on GitHub"
+[ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master "GitLab CE repository"
diff --git a/doc/workflow/forking/fork_button.png b/doc/workflow/forking/fork_button.png
deleted file mode 100644
index def4266476a..00000000000
--- a/doc/workflow/forking/fork_button.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/forking/groups.png b/doc/workflow/forking/groups.png
deleted file mode 100644
index 3ac64b3c8e7..00000000000
--- a/doc/workflow/forking/groups.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md
index 8edf7c6ab3d..217a4a4012f 100644
--- a/doc/workflow/forking_workflow.md
+++ b/doc/workflow/forking_workflow.md
@@ -1,36 +1,59 @@
# Project forking workflow
-Forking a project to your own namespace is useful if you have no write access to the project you want to contribute
-to. If you do have write access or can request it we recommend working together in the same repository since it is simpler.
-See our **[GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)** article for more information about using
-branches to work together.
+Forking a project to your own namespace is useful if you have no write
+access to the project you want to contribute to. If you do have write
+access or can request it, we recommend working together in the same
+repository since it is simpler. See our [GitLab Flow](gitlab_flow.md)
+document more information about using branches to work together.
## Creating a fork
-In order to create a fork of a project, all you need to do is click on the fork button located on the top right side
-of the screen, close to the project's URL and right next to the stars button.
+Forking a project is in most cases a two-step process.
-![Fork button](forking/fork_button.png)
-Once you do that you'll be presented with a screen where you can choose the namespace to fork to. Only namespaces
-(groups and your own namespace) where you have write access to, will be shown. Click on the namespace to create your
-fork there.
+1. Click on the fork button located in the middle of the page or a project's
+ home page right next to the stars button.
-![Groups view](forking/groups.png)
+ ![Fork button](img/forking_workflow_fork_button.png)
-After the forking is done, you can start working on the newly created repository. There you will have full
-[Owner](../permissions/permissions.md) access, so you can set it up as you please.
+ ---
+
+1. Once you do that, you'll be presented with a screen where you can choose
+ the namespace to fork to. Only namespaces (groups and your own
+ namespace) where you have write access to, will be shown. Click on the
+ namespace to create your fork there.
+
+ ![Choose namespace](img/forking_workflow_choose_namespace.png)
+
+ ---
+
+ **Note:**
+ If the namespace you chose to fork the project to has another project with
+ the same path name, you will be presented with a warning that the forking
+ could not be completed. Try to resolve the error and repeat the forking
+ process.
+
+ ![Path taken error](img/forking_workflow_path_taken_error.png)
+
+ ---
+
+After the forking is done, you can start working on the newly created
+repository. There, you will have full [Owner](../permissions/permissions.md)
+access, so you can set it up as you please.
## Merging upstream
-Once you are ready to send your code back to the main project, you need to create a merge request. Choose your forked
-project's main branch as the source and the original project's main branch as the destination and create the merge request.
+Once you are ready to send your code back to the main project, you need
+to create a merge request. Choose your forked project's main branch as
+the source and the original project's main branch as the destination and
+create the [merge request](merge_requests.md).
![Selecting branches](forking/branch_select.png)
-You can then assign the merge request to someone to have them review your changes. Upon pressing the 'Accept Merge Request'
-button, your changes will be added to the repository and branch you're merging into.
+You can then assign the merge request to someone to have them review
+your changes. Upon pressing the 'Accept Merge Request' button, your
+changes will be added to the repository and branch you're merging into.
![New merge request](forking/merge_request.png)
-
+[gitlab flow]: https://about.gitlab.com/2014/09/29/gitlab-flow/ "GitLab Flow blog post"
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 8965e5b3654..1b354bcc0f1 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -16,7 +16,7 @@ It offers a simple, transparent and effective way to work with git.
![Four stages (working copy, index, local repo, remote repo) and three steps between them](four_stages.png)
When converting to git you have to get used to the fact that there are three steps before a commit is shared with colleagues.
-Most version control systems have only step, committing from the working copy to a shared server.
+Most version control systems have only one step, committing from the working copy to a shared server.
In git you add files from the working copy to the staging area. After that you commit them to the local repo.
The third step is pushing to a shared remote repository.
After getting used to these three steps the branching model becomes the challenge.
@@ -152,9 +152,10 @@ The name of this branch should start with the issue number, for example '15-requ
When you are done or want to discuss the code you open a merge request.
This is an online place to discuss the change and review the code.
-Creating a branch is a manual action since you do not always want to merge a new branch you push, it could be a long-running environment or release branch.
-If you create the merge request but do not assign it to anyone it is a 'work-in-process' merge request.
+Opening a merge request is a manual action since you do not always want to merge a new branch you push, it could be a long-running environment or release branch.
+If you open the merge request but do not assign it to anyone it is a 'Work In Progress' merge request.
These are used to discuss the proposed implementation but are not ready for inclusion in the master branch yet.
+_Pro tip:_ Start the title of the merge request with `[WIP]` or `WIP:` to prevent it from being merged before it's ready.
When the author thinks the code is ready the merge request is assigned to reviewer.
The reviewer presses the merge button when they think the code is ready for inclusion in the master branch.
@@ -185,13 +186,16 @@ If you have an issue that spans across multiple repositories, the best thing is
![Vim screen showing the rebase view](rebase.png)
-With git you can use an interactive rebase (rebase -i) to squash multiple commits into one and reorder them.
+With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them.
+In GitLab EE and .com you can also [rebase before merge](http://doc.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface.
This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical.
However you should never rebase commits you have pushed to a remote server.
Somebody can have referred to the commits or cherry-picked them.
When you rebase you change the identifier (SHA-1) of the commit and this is confusing.
If you do that the same change will be known under multiple identifiers and this can cause much confusion.
If people already reviewed your code it will be hard for them to review only the improvements you made since then if you have rebased everything into one commit.
+Another reasons not to rebase is that you lose authorship information, maybe someone created a merge request, another person pushed a commit on there to improve it and a third one merged it.
+In this case rebasing all the commits into one prevent the other authors from being properly attributed and sharing part of the [git blame](https://git-scm.com/docs/git-blame).
People are encouraged to commit often and to frequently push to the remote repository so other people are aware what everyone is working on.
This will lead to many commits per change which makes the history harder to understand.
@@ -220,13 +224,11 @@ You can reuse recorded resolutions (rerere) sometimes, but without rebasing you
There has to be a better way to avoid many merge commits.
The way to prevent creating many merge commits is to not frequently merge master into the feature branch.
-We'll discuss the three reasons to merge in master: leveraging code, solving merge conflicts and long running branches.
+We'll discuss the three reasons to merge in master: leveraging code, merge conflicts, and long running branches.
If you need to leverage some code that was introduced in master after you created the feature branch you can sometimes solve this by just cherry-picking a commit.
If your feature branch has a merge conflict, creating a merge commit is a normal way of solving this.
-You should aim to prevent merge conflicts where they are likely to occur.
-One example is the CHANGELOG file where each significant change in the codebase is documented under a version header.
-Instead of everyone adding their change at the bottom of the list for the current version it is better to randomly insert it in the current list for that version.
-This it is likely that multiple feature branches that add to the CHANGELOG can be merged before a conflict occurs.
+You can prevent some merge conflicts by using [gitattributes](http://git-scm.com/docs/gitattributes) for files that can be in a random order.
+For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG merge=union` so that there are fewer merge conflicts in it.
The last reason for creating merge commits is having long lived branches that you want to keep up to date with the latest state of the project.
Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI).
At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit.
diff --git a/doc/workflow/groups/max_access_level.png b/doc/workflow/groups/max_access_level.png
new file mode 100644
index 00000000000..71106a8a5a0
--- /dev/null
+++ b/doc/workflow/groups/max_access_level.png
Binary files differ
diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/workflow/groups/other_group_sees_shared_project.png
new file mode 100644
index 00000000000..cbf2c3c1fdc
--- /dev/null
+++ b/doc/workflow/groups/other_group_sees_shared_project.png
Binary files differ
diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png
new file mode 100644
index 00000000000..a5dbc89fe90
--- /dev/null
+++ b/doc/workflow/groups/share_project_with_groups.png
Binary files differ
diff --git a/doc/workflow/img/file_finder_find_button.png b/doc/workflow/img/file_finder_find_button.png
new file mode 100644
index 00000000000..c5005d0d7ca
--- /dev/null
+++ b/doc/workflow/img/file_finder_find_button.png
Binary files differ
diff --git a/doc/workflow/img/file_finder_find_file.png b/doc/workflow/img/file_finder_find_file.png
new file mode 100644
index 00000000000..58500f4c163
--- /dev/null
+++ b/doc/workflow/img/file_finder_find_file.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_choose_namespace.png b/doc/workflow/img/forking_workflow_choose_namespace.png
new file mode 100644
index 00000000000..eefe5769554
--- /dev/null
+++ b/doc/workflow/img/forking_workflow_choose_namespace.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_fork_button.png b/doc/workflow/img/forking_workflow_fork_button.png
new file mode 100644
index 00000000000..49e68d33e89
--- /dev/null
+++ b/doc/workflow/img/forking_workflow_fork_button.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_path_taken_error.png b/doc/workflow/img/forking_workflow_path_taken_error.png
new file mode 100644
index 00000000000..7a3139506fe
--- /dev/null
+++ b/doc/workflow/img/forking_workflow_path_taken_error.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_commit.png b/doc/workflow/img/revert_changes_commit.png
new file mode 100644
index 00000000000..d84211e20db
--- /dev/null
+++ b/doc/workflow/img/revert_changes_commit.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_commit_modal.png b/doc/workflow/img/revert_changes_commit_modal.png
new file mode 100644
index 00000000000..e94d151a2af
--- /dev/null
+++ b/doc/workflow/img/revert_changes_commit_modal.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_mr.png b/doc/workflow/img/revert_changes_mr.png
new file mode 100644
index 00000000000..7adad88463b
--- /dev/null
+++ b/doc/workflow/img/revert_changes_mr.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_mr_modal.png b/doc/workflow/img/revert_changes_mr_modal.png
new file mode 100644
index 00000000000..9da78f84828
--- /dev/null
+++ b/doc/workflow/img/revert_changes_mr_modal.png
Binary files differ
diff --git a/doc/workflow/img/todos_icon.png b/doc/workflow/img/todos_icon.png
new file mode 100644
index 00000000000..879b3b51c21
--- /dev/null
+++ b/doc/workflow/img/todos_icon.png
Binary files differ
diff --git a/doc/workflow/img/todos_index.png b/doc/workflow/img/todos_index.png
new file mode 100644
index 00000000000..4ee18dd1285
--- /dev/null
+++ b/doc/workflow/img/todos_index.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_branch_dropdown.png b/doc/workflow/img/web_editor_new_branch_dropdown.png
new file mode 100644
index 00000000000..009e4b05adf
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_branch_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_branch_page.png b/doc/workflow/img/web_editor_new_branch_page.png
new file mode 100644
index 00000000000..dd6cfc6e7bb
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_branch_page.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_directory_dialog.png b/doc/workflow/img/web_editor_new_directory_dialog.png
new file mode 100644
index 00000000000..2c76f84f395
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_directory_dialog.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_directory_dropdown.png b/doc/workflow/img/web_editor_new_directory_dropdown.png
new file mode 100644
index 00000000000..cedf46aedfd
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_directory_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_file_dropdown.png b/doc/workflow/img/web_editor_new_file_dropdown.png
new file mode 100644
index 00000000000..6e884f6504d
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_file_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_file_editor.png b/doc/workflow/img/web_editor_new_file_editor.png
new file mode 100644
index 00000000000..c76473bcfa7
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_file_editor.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_push_widget.png b/doc/workflow/img/web_editor_new_push_widget.png
new file mode 100644
index 00000000000..a2108735741
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_push_widget.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_tag_dropdown.png b/doc/workflow/img/web_editor_new_tag_dropdown.png
new file mode 100644
index 00000000000..263dd635b95
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_tag_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_tag_page.png b/doc/workflow/img/web_editor_new_tag_page.png
new file mode 100644
index 00000000000..64d7cd11ed1
--- /dev/null
+++ b/doc/workflow/img/web_editor_new_tag_page.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_start_new_merge_request.png b/doc/workflow/img/web_editor_start_new_merge_request.png
new file mode 100644
index 00000000000..be12a151cac
--- /dev/null
+++ b/doc/workflow/img/web_editor_start_new_merge_request.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_upload_file_dialog.png b/doc/workflow/img/web_editor_upload_file_dialog.png
new file mode 100644
index 00000000000..6dd2207bca0
--- /dev/null
+++ b/doc/workflow/img/web_editor_upload_file_dialog.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_upload_file_dropdown.png b/doc/workflow/img/web_editor_upload_file_dropdown.png
new file mode 100644
index 00000000000..bf6528701b0
--- /dev/null
+++ b/doc/workflow/img/web_editor_upload_file_dropdown.png
Binary files differ
diff --git a/doc/workflow/importing/github_importer/importer.png b/doc/workflow/importing/github_importer/importer.png
deleted file mode 100644
index 57636717571..00000000000
--- a/doc/workflow/importing/github_importer/importer.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/github_importer/new_project_page.png b/doc/workflow/importing/github_importer/new_project_page.png
deleted file mode 100644
index 002f22d81d7..00000000000
--- a/doc/workflow/importing/github_importer/new_project_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/workflow/importing/img/import_projects_from_github_importer.png
new file mode 100644
index 00000000000..f744dc06f81
--- /dev/null
+++ b/doc/workflow/importing/img/import_projects_from_github_importer.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
new file mode 100644
index 00000000000..86be35acb37
--- /dev/null
+++ b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
Binary files differ
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 1e9825e2e10..520c4216295 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -1,6 +1,6 @@
# Import your project from Bitbucket to GitLab
-It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](doc/integration/bitbucket.md).
+It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md).
* Sign in to GitLab.com and go to your dashboard
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 2d77c6d1172..f693f430a42 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -1,20 +1,44 @@
# Import your project from GitHub to GitLab
-It takes just a couple of steps to import your existing GitHub projects to GitLab. Keep in mind that it is possible only if
-GitHub support is enabled on your GitLab instance. You can read more about GitHub support [here](http://doc.gitlab.com/ce/integration/github.html)
+_**Note:** In order to enable the GitHub import setting, you should first
+enable the [GitHub integration][gh-import] in your GitLab instance._
-If you want to import from a GitHub Enterprise instance, you need to use GitLab Enterprise; please see the [EE docs for the GitHub integration](http://doc.gitlab.com/ee/integration/github.html).
+At its current state, GitHub importer can import:
-* Sign in to GitLab.com and go to your dashboard.
-* To get to the importer page, you need to go to the "New project" page.
+- the repository description (introduced in GitLab 7.7)
+- the git repository data (introduced in GitLab 7.7)
+- the issues (introduced in GitLab 7.7)
+- the pull requests (introduced in GitLab 8.4)
+- the wiki pages (introduced in GitLab 8.4)
-![New project page](github_importer/new_project_page.png)
+It is not yet possible to import your labels, milestones and cross-repository
+pull requests (those from forks). We are working on improving this in the near
+future.
-* Click on the "Import project from GitHub" link and you will be redirected to GitHub for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
+The importer page is visible when you [create a new project][new-project].
+Click on the **GitHub** link and you will be redirected to GitHub for
+permission to access your projects. After accepting, you'll be automatically
+redirected to the importer.
-![Importer page](github_importer/importer.png)
+![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
-* To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data.
+---
-### Note
-When you import your projects from GitHub, it is not possible to keep your labels and milestones. We are working on improving this in the near future.
+While at the GitHub importer page, you can see the import statuses of your
+GitHub projects. Those that are being imported will show a _started_ status,
+those already imported will be green, whereas those that are not yet imported
+have an **Import** button on the right side of the table. If you want, you can
+import all your GitHub projects in one go by hitting **Import all projects**
+in the upper left corner.
+
+![GitHub importer page](img/import_projects_from_github_importer.png)
+
+---
+
+The importer will create any new namespaces if they don't exist or in the
+case the namespace is taken, the project will be imported on the user's
+namespace.
+
+[gh-import]: ../../integration/github.md "GitHub integration"
+[ee-gh]: http://doc.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
+[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index b355a91b5a6..4828bb5dce6 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -69,6 +69,7 @@ branches and tags.
```bash
git remote add origin git@gitlab.com:<group>/<project>.git
git push --all origin
+git push --tags origin
```
## Contribute to this guide
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 5076b2697a3..36cb9da2380 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -9,7 +9,8 @@ Documentation on how to use Git LFS are under [Managing large binary files with
## Configuration
-Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on.
+Git LFS objects can be large in size. By default, they are stored on the server
+GitLab is installed on.
There are two configuration options to help GitLab server administrators:
@@ -37,5 +38,8 @@ In `config/gitlab.yml`:
## Known limitations
-* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported
+* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
+ is not supported
* Currently, removing LFS objects from GitLab Git LFS storage is not supported
+* LFS authentications via SSH is not supported for the time being
+* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2.
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index b59e92cb317..ba91685a20b 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -1,17 +1,21 @@
# Git LFS
-Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git.
-The general recommendation is to not have Git repositories larger than 1GB to preserve performance.
+Managing large files such as audio, video and graphics files has always been one
+of the shortcomings of Git. The general recommendation is to not have Git repositories
+larger than 1GB to preserve performance.
-GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain
-environments it is not always convenient to use different commands to differentiate between the large files and regular ones.
+GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html)
+(EE only), however in certain environments it is not always convenient to use
+different commands to differentiate between the large files and regular ones.
-Git LFS makes this simpler for the end user by removing the requirement to learn new commands.
+Git LFS makes this simpler for the end user by removing the requirement to
+learn new commands.
## How it works
-Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests.
-Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file.
+Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
+to authorize client requests. Once the request is authorized, Git LFS client receives
+instructions from where to fetch or where to push the large file.
## GitLab server configuration
@@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do
## Known limitations
-* Git LFS v1 original API is not supported since it was deprecated early in LFS development
+* Git LFS v1 original API is not supported since it was deprecated early in LFS
+ development
* When SSH is set as a remote, Git LFS objects still go through HTTPS
-* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended
-* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting)
+* Any Git LFS request will ask for HTTPS credentials to be provided so good Git
+ credentials store is recommended
+* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
+ to add the URL to Git config manually (see #troubleshooting)
## Using Git LFS
-Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS:
-For example, if you want to upload a very large file and check it into your Git repository:
+Lets take a look at the workflow when you need to check large files into your Git
+repository with Git LFS. For example, if you want to upload a very large file and
+check it into your Git repository:
```bash
git clone git@gitlab.example.com:group/project.git
@@ -40,7 +48,8 @@ git lfs init # initialize the Git LFS project project
git lfs track "*.iso" # select the file extensions that you want to treat as large files
```
-Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension:
+Once a certain file extension is marked for tracking as a LFS object you can use
+Git as usual without having to redo the command to track a file with the same extension:
```bash
cp ~/tmp/debian.iso ./ # copy a large file into the current directory
@@ -49,13 +58,17 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server
```
-Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication.
+Cloning the repository works the same as before. Git automatically detects the
+LFS-tracked files and clones them via HTTP. If you performed the git clone
+command with a SSH URL, you have to enter your GitLab credentials for HTTP
+authentication.
```bash
git clone git@gitlab.example.com:group/project.git
```
-If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`:
+If you already cloned the repository and you want to get the latest LFS object
+that are on the remote repository, eg. from branch `master`:
```bash
git lfs fetch master
@@ -73,8 +86,8 @@ Check if you have permissions to push to the project or fetch from the project.
* Project is not allowed to access the LFS object
-LFS object you are trying to push to the project or fetch from the project is not available to the project anymore.
-Probably the object was removed from the server.
+LFS object you are trying to push to the project or fetch from the project is not
+available to the project anymore. Probably the object was removed from the server.
* Local git repository is using deprecated LFS API
@@ -89,16 +102,26 @@ git lfs logs last
If the status `error 501` is shown, it is because:
-* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support.
+* Git LFS support is not enabled on the GitLab server. Check with your GitLab
+ administrator why Git LFS is not enabled on the server. See
+ [LFS administration documentation](lfs_administration.md) for instructions
+ on how to enable LFS support.
-* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try to update your Git LFS client. Only version 1.0.1 and newer are supported.
+* Git LFS client version is not supported by GitLab server. Check your Git LFS
+ version with `git lfs version`. Check the Git config of the project for traces
+ of deprecated API with `git lfs -l`. If `batch = false` is set in the config,
+ remove the line and try to update your Git LFS client. Only version 1.0.1 and
+ newer are supported.
### getsockopt: connection refused
-If you push a LFS object to a project and you receive an error similar to: `Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
-the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP.
+If you push a LFS object to a project and you receive an error similar to:
+`Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
+the LFS client is trying to reach GitLab through HTTPS. However, your GitLab
+instance is being served on HTTP.
-This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config.
+This behaviour is caused by Git LFS using HTTPS connections by default when a
+`lfsurl` is not set in the Git config.
To prevent this from happening, set the lfs url in project Git config:
@@ -109,18 +132,24 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/o
### Credentials are always required when pushing an object
-Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required.
+Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
+the LFS object on every push for every object, user HTTPS credentials are required.
-By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
+By default, Git has support for remembering the credentials for each repository
+you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
-For example, you can tell Git to remember the password for a period of time in which you expect to push the objects:
+For example, you can tell Git to remember the password for a period of time in
+which you expect to push the objects:
```bash
git config --global credential.helper 'cache --timeout=3600'
```
-This will remember the credentials for an hour after which Git operations will require re-authentication.
+This will remember the credentials for an hour after which Git operations will
+require re-authentication.
-If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
+If you are using OS X you can use `osxkeychain` to store and encrypt your credentials.
+For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
-More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file
+More details about various methods of storing the user credentials can be found
+on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file
diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md
index 0adf9f8e3e8..d854ec1e025 100644
--- a/doc/workflow/protected_branches.md
+++ b/doc/workflow/protected_branches.md
@@ -1,6 +1,6 @@
# Protected branches
-Permission in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches.
+Permissions in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches.
To prevent people from messing with history or pushing code without review, we've created protected branches.
@@ -12,7 +12,7 @@ A protected branch does three simple things:
You can make any branch a protected branch. GitLab makes the master branch a protected branch by default.
-To protect a branch, user needs to have at least a Master permission level, see [permissions document](doc/permissions/permissions.md).
+To protect a branch, user needs to have at least a Master permission level, see [permissions document](../permissions/permissions.md).
![protected branches page](protected_branches/protected_branches1.png)
diff --git a/doc/workflow/revert_changes.md b/doc/workflow/revert_changes.md
new file mode 100644
index 00000000000..399366b0cdc
--- /dev/null
+++ b/doc/workflow/revert_changes.md
@@ -0,0 +1,64 @@
+# Reverting changes
+
+_**Note:** This feature was [introduced][ce-1990] in GitLab 8.5._
+
+---
+
+GitLab implements Git's powerful feature to [revert any commit][git-revert]
+with introducing a **Revert** button in Merge Requests and commit details.
+
+## Reverting a Merge Request
+
+_**Note:** The **Revert** button will only be available for Merge Requests
+created since GitLab 8.5. However, you can still revert a Merge Request
+by reverting the merge commit from the list of Commits page._
+
+After the Merge Request has been merged, a **Revert** button will be available
+to revert the changes introduced by that Merge Request:
+
+![Revert Merge Request](img/revert_changes_mr.png)
+
+---
+
+You can revert the changes directly into the selected branch or you can opt to
+create a new Merge Request with the revert changes:
+
+![Revert Merge Request modal](img/revert_changes_mr_modal.png)
+
+---
+
+After the Merge Request has been reverted, the **Revert** button will not be
+available anymore.
+
+## Reverting a Commit
+
+You can revert a Commit from the Commit details page:
+
+![Revert commit](img/revert_changes_commit.png)
+
+---
+
+Similar to reverting a Merge Request, you can opt to revert the changes
+directly into the target branch or create a new Merge Request to revert the
+changes:
+
+![Revert commit modal](img/revert_changes_commit_modal.png)
+
+---
+
+After the Commit has been reverted, the **Revert** button will not be available
+anymore.
+
+Please note that when reverting merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to revert a merge commit using the second parent as the
+mainline:
+
+```bash
+git revert -m 2 7a39eb0
+```
+
+[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request"
+[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation"
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
new file mode 100644
index 00000000000..4c59f59c587
--- /dev/null
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -0,0 +1,30 @@
+# Share Projects with other Groups
+
+In GitLab Enterprise Edition you can share projects with other groups.
+This makes it possible to add a group of users to a project with a single action.
+
+## Groups as collections of users
+
+In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
+In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
+
+## Sharing a project with a group of users
+
+The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
+But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the (Enterprise Edition only) group sharing feature can be of use.
+
+To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
+
+![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
+
+Now you can add the 'Engineering' group with the maximum access level of your choice.
+After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
+
+!['Project Acme' is listed as a shared project for 'Engineering'](groups/other_group_sees_shared_project.png)
+
+## Maximum access level
+
+!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](groups/max_access_level.png)
+
+In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
diff --git a/doc/workflow/share_with_group.md b/doc/workflow/share_with_group.md
new file mode 100644
index 00000000000..3b7690973cb
--- /dev/null
+++ b/doc/workflow/share_with_group.md
@@ -0,0 +1,13 @@
+# Sharing a project with a group
+
+If you want to share a single project in a group with another group,
+you can do so easily. By setting the permission you can quickly
+give a select group of users access to a project in a restricted manner.
+
+In a project go to the project settings -> groups.
+
+Now you can select a group that you want to share this project with and with
+which maximum access level. Users in that group are able to access this project
+with their set group access level, up to the maximum level that you've set.
+
+![Share a project with a group](share_with_group.png)
diff --git a/doc/workflow/share_with_group.png b/doc/workflow/share_with_group.png
new file mode 100644
index 00000000000..a0ca6f14552
--- /dev/null
+++ b/doc/workflow/share_with_group.png
Binary files differ
diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png
index 68756ed1f98..83e562d6929 100644
--- a/doc/workflow/shortcuts.png
+++ b/doc/workflow/shortcuts.png
Binary files differ
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
new file mode 100644
index 00000000000..5f440fdafdd
--- /dev/null
+++ b/doc/workflow/todos.md
@@ -0,0 +1,73 @@
+# GitLab ToDos
+
+>**Note:** This feature was [introduced][ce-2817] in GitLab 8.5.
+
+When you log into GitLab, you normally want to see where you should spend your
+time and take some action, or what you need to keep an eye on. All without the
+mess of a huge pile of e-mail notifications. GitLab is where you do your work,
+so being able to get started quickly is very important.
+
+Todos is a chronological list of to-dos that are waiting for your input, all
+in a simple dashboard.
+
+![Todos screenshot showing a list of items to check on](img/todos_index.png)
+
+---
+
+You can access quickly your Todos dashboard by clicking the round gray icon
+next to the search bar in the upper right corner.
+
+![Todos icon](img/todos_icon.png)
+
+## What triggers a Todo
+
+A Todo appears in your Todos dashboard when:
+
+- an issue or merge request is assigned to you
+- you are `@mentioned` in an issue or merge request, be it the description of
+ the issue/merge request or in a comment
+
+>**Note:** Commenting on a commit will _not_ trigger a Todo.
+
+## How a Todo is marked as Done
+
+Any action to the corresponding issue or merge request will mark your Todo as
+**Done**. This action can include:
+
+- changing the assignee
+- changing the milestone
+- adding/removing a label
+- commenting on the issue
+
+In case where you think no action is needed, you can manually mark the todo as
+done by clicking the corresponding **Done** button, and it will disappear from
+your Todos list. If you want to mark all your Todos as done, just click on the
+**Mark all as done** button.
+
+---
+
+In order for a Todo to be marked as done, the action must be coming from you.
+So, if you close the related issue or merge the merge request yourself, and you
+had a Todo for that, it will automatically get marked as done. On the other
+hand, if someone else closes, merges or takes action on the issue or merge
+request, your Todo will remain pending. This makes sense because you may need
+to give attention to an issue even if it has been resolved.
+
+There is just one Todo per issue or merge request, so mentioning a user a
+hundred times in an issue will only trigger one Todo.
+
+## Filtering your Todos
+
+In general, there are four kinds of filters you can use on your Todos
+dashboard:
+
+| Filter | Description |
+| ------ | ----------- |
+| Project | Filter by project |
+| Author | Filter by the author that triggered the Todo |
+| Type | Filter by issue or merge request |
+| Action | Filter by the action that triggered the Todo (Assigned or Mentioned)|
+
+You can choose more than one filters at the same time.
+
+[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md
index 7fc8f96b9ec..4a451d98953 100644
--- a/doc/workflow/web_editor.md
+++ b/doc/workflow/web_editor.md
@@ -1,26 +1,120 @@
# GitLab Web Editor
-In GitLab you can create new files and edit existing files using our web editor.
-This is especially useful if you don't have access to a command line or you just want to do a quick fix.
-You can easily access the web editor, depending on the context.
-Let's start from newly created project.
+Sometimes it's easier to make quick changes directly from the GitLab interface
+than to clone the project and use the Git command line tool. In this feature
+highlight we look at how you can create a new file, directory, branch or
+tag from the file browser. All of these actions are available from a single
+dropdown menu.
-Click on `Add a file`
-to create the first file and open it in the web editor.
+## Create a file
-![web editor 1](web_editor/empty_project.png)
+From a project's files page, click the '+' button to the right of the branch selector.
+Choose **New file** from the dropdown.
-Fill in a file name, some content, a commit message, branch name and press the commit button.
-The file will be saved to the repository.
+![New file dropdown menu](img/web_editor_new_file_dropdown.png)
-![web editor 2](web_editor/new_file.png)
+---
-You can edit any text file in a repository by pressing the edit button, when
-viewing the file.
+Enter a file name in the **File name** box. Then, add file content in the editor
+area. Add a descriptive commit message and choose a branch. The branch field
+will default to the branch you were viewing in the file browser. If you enter
+a new branch name, a checkbox will appear allowing you to start a new merge
+request after you commit the changes.
-![web editor 3](web_editor/show_file.png)
+When you are satisfied with your new file, click **Commit Changes** at the bottom.
-Editing a file is almost the same as creating a new file,
-with as addition the ability to preview your changes in a separate tab. Also you can save your change to another branch by filling out field `branch`
+![Create file editor](img/web_editor_new_file_editor.png)
-![web editor 3](web_editor/edit_file.png)
+## Upload a file
+
+The ability to create a file is great when the content is text. However, this
+doesn't work well for binary data such as images, PDFs or other file types. In
+this case you need to upload a file.
+
+From a project's files page, click the '+' button to the right of the branch
+selector. Choose **Upload file** from the dropdown.
+
+![Upload file dropdown menu](img/web_editor_upload_file_dropdown.png)
+
+---
+
+Once the upload dialog pops up there are two ways to upload your file. Either
+drag and drop a file on the pop up or use the **click to upload** link. A file
+preview will appear once you have selected a file to upload.
+
+Enter a commit message, choose a branch, and click **Upload file** when you are
+ready.
+
+![Upload file dialog](img/web_editor_upload_file_dialog.png)
+
+## Create a directory
+
+To keep files in the repository organized it is often helpful to create a new
+directory.
+
+From a project's files page, click the '+' button to the right of the branch selector.
+Choose **New directory** from the dropdown.
+
+![New directory dropdown](img/web_editor_new_directory_dropdown.png)
+
+---
+
+In the new directory dialog enter a directory name, a commit message and choose
+the target branch. Click **Create directory** to finish.
+
+![New directory dialog](img/web_editor_new_directory_dialog.png)
+
+## Create a new branch
+
+If you want to make changes to several files before creating a new merge
+request, you can create a new branch up front. From a project's files page,
+choose **New branch** from the dropdown.
+
+![New branch dropdown](img/web_editor_new_branch_dropdown.png)
+
+---
+
+Enter a new **Branch name**. Optionally, change the **Create from** field
+to choose which branch, tag or commit SHA this new branch will originate from.
+This field will autocomplete if you start typing an existing branch or tag.
+Click **Create branch** and you will be returned to the file browser on this new
+branch.
+
+![New branch page](img/web_editor_new_branch_page.png)
+
+---
+
+You can now make changes to any files, as needed. When you're ready to merge
+the changes back to master you can use the widget at the top of the screen.
+This widget only appears for a period of time after you create the branch or
+modify files.
+
+![New push widget](img/web_editor_new_push_widget.png)
+
+## Create a new tag
+
+Tags are useful for marking major milestones such as production releases,
+release candidates, and more. You can create a tag from a branch or a commit
+SHA. From a project's files page, choose **New tag** from the dropdown.
+
+![New tag dropdown](img/web_editor_new_tag_dropdown.png)
+
+---
+
+Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you
+would like to create this new tag. You can optionally add a message and
+release notes. The release notes section supports markdown format and you can
+also upload an attachment. Click **Create tag** and you will be taken to the tag
+list page.
+
+![New tag page](img/web_editor_new_tag_page.png)
+
+## Tips
+
+When creating or uploading a new file, or creating a new directory, you can
+trigger a new merge request rather than committing directly to master. Enter
+a new branch name in the **Target branch** field. You will notice a checkbox
+appear that is labeled **Start a new merge request with these changes**. After
+you commit the changes you will be taken to a new merge request form.
+
+![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
diff --git a/doc/workflow/web_editor/edit_file.png b/doc/workflow/web_editor/edit_file.png
deleted file mode 100644
index f480c69ac3e..00000000000
--- a/doc/workflow/web_editor/edit_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/web_editor/empty_project.png b/doc/workflow/web_editor/empty_project.png
deleted file mode 100644
index 6a049f6beaf..00000000000
--- a/doc/workflow/web_editor/empty_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/web_editor/new_file.png b/doc/workflow/web_editor/new_file.png
deleted file mode 100644
index 55ebd9e0257..00000000000
--- a/doc/workflow/web_editor/new_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/web_editor/show_file.png b/doc/workflow/web_editor/show_file.png
deleted file mode 100644
index 9cafcb55109..00000000000
--- a/doc/workflow/web_editor/show_file.png
+++ /dev/null
Binary files differ
diff --git a/doc_styleguide.md b/doc_styleguide.md
index cceb449a854..05ff46323ac 100644
--- a/doc_styleguide.md
+++ b/doc_styleguide.md
@@ -1,26 +1,3 @@
# Documentation styleguide
-This styleguide recommends best practices to improve documentation and to keep it organized and easy to find.
-
-## Text
-
-- Split up long lines, this makes it much easier to review and edit. Only
-double line breaks are shown as a full line break in markdown. 80 characters
-is a good line length.
-- For subtitles, make sure to start with the largest and go down, meaning:
-`#` for the title, `##` for subtitles and `###` for subtitles of the subtitles, etc.
-- Make sure that the documentation is added in the correct directory and that there's a link to it somewhere useful.
-- Add only one H1 or title in each document, by adding '#' at the begining of it (when using markdown).
-For subtitles, use '##', '###' and so on.
-- Do not duplicate information.
-- Be brief and clear.
-- Whenever it applies, add documents in alphabetical order.
-- Write in US English
-- Use [single spaces](http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html) instead of double spaces.
-
-## Images
-
-- Create a directory to store the images with the specific name of the document where the images belong.
-It could be in the same directory where the .md document that you're working on is located.
-- Images should have a specific, non-generic name that will differentiate them.
-- Keep all file names in lower case. \ No newline at end of file
+Moved to [development/doc_styleguide](doc/development/doc_styleguide.md).
diff --git a/features/admin/appearance.feature b/features/admin/appearance.feature
new file mode 100644
index 00000000000..5c1dd7531c1
--- /dev/null
+++ b/features/admin/appearance.feature
@@ -0,0 +1,37 @@
+Feature: Admin Appearance
+ Scenario: Create new appearance
+ Given I sign in as an admin
+ And I visit admin appearance page
+ When submit form with new appearance
+ Then I should be redirected to admin appearance page
+ And I should see newly created appearance
+
+ Scenario: Preview appearance
+ Given application has custom appearance
+ And I sign in as an admin
+ When I visit admin appearance page
+ And I click preview button
+ Then I should see a customized appearance
+
+ Scenario: Custom sign-in page
+ Given application has custom appearance
+ When I visit login page
+ Then I should see a customized appearance
+
+ Scenario: Appearance logo
+ Given application has custom appearance
+ And I sign in as an admin
+ And I visit admin appearance page
+ When I attach a logo
+ Then I should see a logo
+ And I remove the logo
+ Then I should see logo removed
+
+ Scenario: Header logos
+ Given application has custom appearance
+ And I sign in as an admin
+ And I visit admin appearance page
+ When I attach header logos
+ Then I should see header logos
+ And I remove the header logos
+ Then I should see header logos removed
diff --git a/features/admin/broadcast_messages.feature b/features/admin/broadcast_messages.feature
index b2c3112320a..4f9c651561e 100644
--- a/features/admin/broadcast_messages.feature
+++ b/features/admin/broadcast_messages.feature
@@ -2,16 +2,11 @@
Feature: Admin Broadcast Messages
Background:
Given I sign in as an admin
- And application already has admin messages
+ And application already has a broadcast message
And I visit admin messages page
Scenario: See broadcast messages list
- Then I should be all broadcast messages
-
- Scenario: Create a broadcast message
- When submit form with new broadcast message
- Then I should be redirected to admin messages page
- And I should see newly created broadcast message
+ Then I should see all broadcast messages
Scenario: Create a customized broadcast message
When submit form with new customized broadcast message
@@ -19,3 +14,20 @@ Feature: Admin Broadcast Messages
And I should see newly created broadcast message
Then I visit dashboard page
And I should see a customized broadcast message
+
+ Scenario: Edit an existing broadcast message
+ When I edit an existing broadcast message
+ And I change the broadcast message text
+ Then I should be redirected to admin messages page
+ And I should see the updated broadcast message
+
+ Scenario: Remove an existing broadcast message
+ When I remove an existing broadcast message
+ Then I should be redirected to admin messages page
+ And I should not see the removed broadcast message
+
+ @javascript
+ Scenario: Live preview a customized broadcast message
+ When I visit admin messages page
+ And I enter a broadcast message with Markdown
+ Then I should see a live preview of the rendered broadcast message
diff --git a/features/admin/groups.feature b/features/admin/groups.feature
index 2edb3964f70..ab7de7ac315 100644
--- a/features/admin/groups.feature
+++ b/features/admin/groups.feature
@@ -21,6 +21,11 @@ Feature: Admin Groups
When I select user "John Doe" from user list as "Reporter"
Then I should see "John Doe" in team list in every project as "Reporter"
+ Scenario: Shared projects
+ Given group has shared projects
+ When I visit group page
+ Then I should see project shared with group
+
@javascript
Scenario: Remove user from group
Given we have user "John Doe" in group
diff --git a/features/admin/spam_logs.feature b/features/admin/spam_logs.feature
new file mode 100644
index 00000000000..92a5389e3a4
--- /dev/null
+++ b/features/admin/spam_logs.feature
@@ -0,0 +1,8 @@
+Feature: Admin spam logs
+ Background:
+ Given I sign in as an admin
+ And spam logs exist
+
+ Scenario: Browse spam logs
+ When I visit spam logs page
+ Then I should see list of spam logs
diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature
index 69b3a776441..bed9282f1c6 100644
--- a/features/dashboard/archived_projects.feature
+++ b/features/dashboard/archived_projects.feature
@@ -10,3 +10,8 @@ Feature: Dashboard Archived Projects
Scenario: I should see non-archived projects on dashboard
Then I should see "Shop" project link
And I should not see "Forum" project link
+
+ Scenario: I toggle show of archived projects on dashboard
+ When I click "Show archived projects" link
+ Then I should see "Shop" project link
+ And I should see "Forum" project link
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index b667b587c5b..c3b3577c449 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -41,3 +41,33 @@ Feature: Dashboard
And user with name "John Doe" left project "Shop"
When I visit dashboard activity page
Then I should see "John Doe left project Shop" event
+
+ @javascript
+ Scenario: Sorting Issues
+ Given I visit dashboard issues page
+ And I sort the list by "Oldest updated"
+ And I visit dashboard activity page
+ And I visit dashboard issues page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Visiting Project's issues after sorting
+ Given I visit dashboard issues page
+ And I sort the list by "Oldest updated"
+ And I visit project "Shop" issues page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Sorting Merge Requests
+ Given I visit dashboard merge requests page
+ And I sort the list by "Oldest updated"
+ And I visit dashboard activity page
+ And I visit dashboard merge requests page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Visiting Project's merge requests after sorting
+ Given I visit dashboard merge requests page
+ And I sort the list by "Oldest updated"
+ And I visit project "Shop" merge requests page
+ Then The list should be sorted by "Oldest updated"
diff --git a/features/dashboard/event_filters.feature b/features/dashboard/event_filters.feature
index 96399ea21a6..8c3ff64164f 100644
--- a/features/dashboard/event_filters.feature
+++ b/features/dashboard/event_filters.feature
@@ -43,10 +43,16 @@ Feature: Event Filters
And I should not see new member event
When I click "team" event filter
And I visit dashboard activity page
- Then I should see push event
+ Then I should not see push event
And I should see new member event
And I should not see merge request event
When I click "push" event filter
- Then I should not see push event
- And I should see new member event
+ And I visit dashboard activity page
+ Then I should see push event
+ And I should not see new member event
And I should not see merge request event
+ When I click "merge" event filter
+ And I visit dashboard activity page
+ Then I should see merge request event
+ And I should not see push event
+ And I should not see new member event
diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature
new file mode 100644
index 00000000000..1e7b1b50d64
--- /dev/null
+++ b/features/dashboard/todos.feature
@@ -0,0 +1,38 @@
+@dashboard
+Feature: Dashboard Todos
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And "John Doe" is a developer of project "Shop"
+ And "Mary Jane" is a developer of project "Shop"
+ And "Mary Jane" owns private project "Enterprise"
+ And I am a developer of project "Enterprise"
+ And I have todos
+ And I visit dashboard todos page
+
+ @javascript
+ Scenario: I mark todos as done
+ Then I should see todos assigned to me
+ And I mark the todo as done
+ And I click on the "Done" tab
+ Then I should see all todos marked as done
+
+ @javascript
+ Scenario: I filter by project
+ Given I filter by "Enterprise"
+ Then I should not see todos
+
+ @javascript
+ Scenario: I filter by author
+ Given I filter by "John Doe"
+ Then I should not see todos related to "Mary Jane" in the list
+
+ @javascript
+ Scenario: I filter by type
+ Given I filter by "Issue"
+ Then I should not see todos related to "Merge Requests" in the list
+
+ @javascript
+ Scenario: I filter by action
+ Given I filter by "Mentioned"
+ Then I should not see todos related to "Assignments" in the list
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
index a42e59c98f2..5fc9b135601 100644
--- a/features/explore/groups.feature
+++ b/features/explore/groups.feature
@@ -105,15 +105,6 @@ Feature: Explore Groups
When I visit the public groups area
Then I should see group "TestGroup"
- Scenario: I should not see group with internal project in public groups area
- Given group "TestGroup" has internal project "Internal"
- When I visit the public groups area
- Then I should not see group "TestGroup"
-
- Scenario: I should not see group with private project in public groups area
- When I visit the public groups area
- Then I should not see group "TestGroup"
-
Scenario: I should see group with public project in public groups area as user
Given group "TestGroup" has public project "Community"
When I sign in as a user
@@ -125,9 +116,3 @@ Feature: Explore Groups
When I sign in as a user
And I visit the public groups area
Then I should see group "TestGroup"
-
- Scenario: I should not see group with private project in public groups area as user
- When I sign in as a user
- And I visit the public groups area
- Then I should not see group "TestGroup"
-
diff --git a/features/explore/projects.feature b/features/explore/projects.feature
index 629859e960d..092e18d1b86 100644
--- a/features/explore/projects.feature
+++ b/features/explore/projects.feature
@@ -87,6 +87,7 @@ Feature: Explore Projects
Scenario: I visit public project issues page as a non authorized user
Given I visit project "Community" page
+ Then I should not see command line instructions
And I visit "Community" issues page
Then I should see list of issues for "Community" project
@@ -139,4 +140,4 @@ Feature: Explore Projects
When I visit the explore starred projects
Then I should see project "Community"
And I should see project "Internal"
- And I should see project "Archive"
+ And I should not see project "Archive"
diff --git a/features/group/milestones.feature b/features/group/milestones.feature
index 62ea66a783c..d6c05df9840 100644
--- a/features/group/milestones.feature
+++ b/features/group/milestones.feature
@@ -28,3 +28,20 @@ Feature: Group Milestones
And I fill milestone name
When I press create mileston button
Then milestone in each project should be created
+
+ Scenario: I should see Issues listed with labels
+ Given Group has projects with milestones
+ When I visit group "Owned" page
+ And I click on group milestones
+ And I click on one group milestone
+ Then I should see the "bug" label
+ And I should see the "feature" label
+ And I should see the project name in the Issue row
+
+ Scenario: I should see the Labels tab
+ Given Group has projects with milestones
+ When I visit group "Owned" page
+ And I click on group milestones
+ And I click on one group milestone
+ And I click on the "Labels" tab
+ Then I should see the list of labels
diff --git a/features/groups.feature b/features/groups.feature
index c803e952980..419a5d3963d 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -3,6 +3,10 @@ Feature: Groups
Given I sign in as "John Doe"
And "John Doe" is owner of group "Owned"
+ Scenario: I should not see a group if it does not exist
+ 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
@@ -11,6 +15,10 @@ Feature: Groups
Scenario: I should see group "Owned" dashboard list
When I visit group "Owned" page
Then I should see group "Owned" projects list
+
+ @javascript
+ Scenario: I should see group "Owned" activity feed
+ When I visit group "Owned" activity page
And I should see projects activity feed
Scenario: I should see group "Owned" issues list
@@ -18,11 +26,23 @@ Feature: Groups
When I visit group "Owned" issues page
Then I should see issues from group "Owned" assigned to me
+ Scenario: I should not see issues from archived project in "Owned" group issues list
+ Given Group "Owned" has archived project
+ And the archived project have some issues
+ When I visit group "Owned" issues page
+ Then I should not see issues from the archived project
+
Scenario: I should see group "Owned" merge requests list
Given project from group "Owned" has merge requests assigned to me
When I visit group "Owned" merge requests page
Then I should see merge requests from group "Owned" assigned to me
+ Scenario: I should not see merge requests from archived project in "Owned" group merge requests list
+ Given Group "Owned" has archived project
+ And the archived project have some merge_requests
+ When I visit group "Owned" merge requests page
+ Then I should not see merge requests from the archived project
+
Scenario: I should see edit group "Owned" page
When I visit group "Owned" settings page
And I change group "Owned" name to "new-name"
diff --git a/features/login_form.feature b/features/login_form.feature
deleted file mode 100644
index b4d95754482..00000000000
--- a/features/login_form.feature
+++ /dev/null
@@ -1,5 +0,0 @@
-Feature: Login form
- Scenario: I see crowd form
- Given Crowd integration enabled
- When I visit sign in page
- Then I should see Crowd login form \ No newline at end of file
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
index 168d9d30b50..447dd92a458 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -76,8 +76,7 @@ Feature: Profile
Scenario: I can manage application
Given I visit profile applications page
- Then I click on new application button
- And I should see application form
+ Then I should see application form
Then I fill application form out and submit
And I see application
Then I click edit
diff --git a/features/profile/ssh_keys.feature b/features/profile/ssh_keys.feature
index 581503fc5f9..b0d5b748916 100644
--- a/features/profile/ssh_keys.feature
+++ b/features/profile/ssh_keys.feature
@@ -9,7 +9,7 @@ Feature: Profile SSH Keys
Then I should see my ssh keys
Scenario: Add new ssh key
- Given I click link "Add new"
+ Given I should see new ssh key form
And I submit new ssh key "Laptop"
Then I should see new ssh key "Laptop"
diff --git a/features/project/badges/build.feature b/features/project/badges/build.feature
new file mode 100644
index 00000000000..bcf80ed620e
--- /dev/null
+++ b/features/project/badges/build.feature
@@ -0,0 +1,27 @@
+Feature: Project Badges Build
+ Background:
+ Given I sign in as a user
+ And I own a project
+ And project has CI enabled
+ And project has a recent build
+
+ Scenario: I want to see a badge for successfully built project
+ Given recent build is successful
+ When I display builds badge for a master branch
+ Then I should see a build success badge
+
+ Scenario: I want to see a badge for project with failed builds
+ Given recent build failed
+ When I display builds badge for a master branch
+ Then I should see a build failed badge
+
+ Scenario: I want to see a badge for project with running builds
+ Given recent build is successful
+ And project has another build that is running
+ When I display builds badge for a master branch
+ Then I should see a build running badge
+
+ Scenario: I want to see a fresh badge on each request
+ Given recent build is successful
+ When I display builds badge for a master branch
+ Then I should see a badge that has not been cached
diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature
new file mode 100644
index 00000000000..52dc15f2eb6
--- /dev/null
+++ b/features/project/builds/artifacts.feature
@@ -0,0 +1,62 @@
+Feature: Project Builds Artifacts
+ Background:
+ Given I sign in as a user
+ And I own a project
+ And project has CI enabled
+ And project has a recent build
+
+ Scenario: I download build artifacts
+ Given recent build has artifacts available
+ When I visit recent build details page
+ And I click artifacts download button
+ Then download of build artifacts archive starts
+
+ Scenario: I browse build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build details page
+ And I click artifacts browse button
+ Then I should see content of artifacts archive
+
+ Scenario: I browse subdirectory of build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build details page
+ And I click artifacts browse button
+ And I click link to subdirectory within build artifacts
+ Then I should see content of subdirectory within artifacts archive
+
+ Scenario: I browse directory with UTF-8 characters in name
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ And recent build artifacts contain directory with UTF-8 characters
+ When I visit recent build details page
+ And I click artifacts browse button
+ And I navigate to directory with UTF-8 characters in name
+ Then I should see content of directory with UTF-8 characters in name
+
+ Scenario: I try to browse directory with invalid UTF-8 characters in name
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ And recent build artifacts contain directory with invalid UTF-8 characters
+ When I visit recent build details page
+ And I click artifacts browse button
+ And I navigate to parent directory of directory with invalid name
+ Then I should not see directory with invalid name on the list
+
+ Scenario: I download a single file from build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build details page
+ And I click artifacts browse button
+ And I click a link to file within build artifacts
+ Then download of a file extracted from build artifacts should start
+
+ @javascript
+ Scenario: I click on a row in an artifacts table
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build details page
+ And I click artifacts browse button
+ And I click a first row within build artifacts table
+ Then page with a coresponding path is loading
diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature
new file mode 100644
index 00000000000..3c7f72335d9
--- /dev/null
+++ b/features/project/builds/permissions.feature
@@ -0,0 +1,53 @@
+Feature: Project Builds Permissions
+ Background:
+ Given I sign in as a user
+ And project exists in some group namespace
+ And project has CI enabled
+ And project has a recent build
+
+ Scenario: I try to visit build details as guest
+ Given I am member of a project with a guest role
+ When I visit recent build details page
+ Then page status code should be 404
+
+ Scenario: I try to visit project builds page as guest
+ Given I am member of a project with a guest role
+ When I visit project builds page
+ Then page status code should be 404
+
+ Scenario: I try to visit build details of internal project without access to builds
+ Given The project is internal
+ And public access for builds is disabled
+ When I visit recent build details page
+ Then page status code should be 404
+
+ Scenario: I try to visit internal project builds page without access to builds
+ Given The project is internal
+ And public access for builds is disabled
+ When I visit project builds page
+ Then page status code should be 404
+
+ Scenario: I try to visit build details of internal project with access to builds
+ Given The project is internal
+ And public access for builds is enabled
+ When I visit recent build details page
+ Then I see details of a build
+ And I see build trace
+
+ Scenario: I try to visit internal project builds page with access to builds
+ Given The project is internal
+ And public access for builds is enabled
+ When I visit project builds page
+ Then I see the build
+
+ Scenario: I try to download build artifacts as guest
+ Given I am member of a project with a guest role
+ And recent build has artifacts available
+ When I access artifacts download page
+ Then page status code should be 404
+
+ Scenario: I try to download build artifacts as reporter
+ Given I am member of a project with a reporter role
+ And recent build has artifacts available
+ When I access artifacts download page
+ Then download of build artifacts archive starts
diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature
new file mode 100644
index 00000000000..3c029a973df
--- /dev/null
+++ b/features/project/builds/summary.feature
@@ -0,0 +1,26 @@
+Feature: Project Builds Summary
+ Background:
+ Given I sign in as a user
+ And I own a project
+ And project has CI enabled
+ And project has coverage enabled
+ And project has a recent build
+
+ Scenario: I browse build details page
+ When I visit recent build details page
+ Then I see details of a build
+ And I see build trace
+
+ Scenario: I browse project builds page
+ When I visit project builds page
+ Then I see coverage
+ Then I see button to CI Lint
+
+ Scenario: I erase a build
+ Given recent build is successful
+ And recent build has a build trace
+ When I visit recent build details page
+ And I click erase build button
+ Then recent build has been erased
+ And recent build summary does not have artifacts widget
+ And recent build summary contains information saying that build has been erased
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 5bb2d0e976b..a95df038357 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -7,6 +7,26 @@ Feature: Project Commits
Scenario: I browse commits list for master branch
Then I see project commits
+ And I should not see button to create a new merge request
+ Then I click the "Compare" tab
+ And I should not see button to create a new merge request
+
+ Scenario: I browse commits list for feature branch without a merge request
+ Given I visit commits list page for feature branch
+ Then I see feature branch commits
+ And I see button to create a new merge request
+ Then I click the "Compare" tab
+ And I see button to create a new merge request
+
+ Scenario: I browse commits list for feature branch with an open merge request
+ Given project have an open merge request
+ And I visit commits list page for feature branch
+ Then I see feature branch commits
+ And I should not see button to create a new merge request
+ And I should see button to the merge request
+ Then I click the "Compare" tab
+ And I should not see button to create a new merge request
+ And I should see button to the merge request
Scenario: I browse atom feed of commits list for master branch
Given I click atom feed link
@@ -31,6 +51,22 @@ Feature: Project Commits
Then I see inline diff button
@javascript
+ Scenario: I compare branches without a merge request
+ Given I visit compare refs page
+ And I fill compare fields with branches
+ Then I see compared branches
+ And I see button to create a new merge request
+
+ @javascript
+ Scenario: I compare branches with an open merge request
+ Given project have an open merge request
+ And I visit compare refs page
+ And I fill compare fields with branches
+ Then I see compared branches
+ And I should not see button to create a new merge request
+ And I should see button to the merge request
+
+ @javascript
Scenario: I compare refs
Given I visit compare refs page
And I fill compare fields with refs
@@ -55,3 +91,8 @@ Feature: Project Commits
Scenario: I browse a commit with an image
Given I visit a commit with an image that changed
Then The diff links to both the previous and current image
+
+ @javascript
+ Scenario: I filter commits by message
+ When I search "submodules" commits
+ Then I should see only "submodules" commits
diff --git a/features/project/commits/revert.feature b/features/project/commits/revert.feature
new file mode 100644
index 00000000000..7a2effafe03
--- /dev/null
+++ b/features/project/commits/revert.feature
@@ -0,0 +1,28 @@
+@project_commits
+Feature: Revert Commits
+ Background:
+ Given I sign in as a user
+ And I own a project
+ And I visit my project's commits page
+
+ Scenario: I revert a commit
+ Given I click on commit link
+ And I click on the revert button
+ And I revert the changes directly
+ Then I should see the revert commit notice
+
+ Scenario: I revert a commit that was previously reverted
+ Given I click on commit link
+ And I click on the revert button
+ And I revert the changes directly
+ And I visit my project's commits page
+ And I click on commit link
+ And I click on the revert button
+ And I revert the changes directly
+ Then I should see a revert error
+
+ Scenario: I revert a commit in a new merge request
+ Given I click on commit link
+ And I click on the revert button
+ And I revert the changes in a new merge request
+ Then I should see the new merge request notice
diff --git a/features/project/find_file.feature b/features/project/find_file.feature
new file mode 100644
index 00000000000..ae8fa245923
--- /dev/null
+++ b/features/project/find_file.feature
@@ -0,0 +1,42 @@
+@dashboard
+Feature: Project Find File
+ Background:
+ Given I sign in as a user
+ And I own a project
+ And I visit my project's files page
+
+ @javascript
+ Scenario: Navigate to find file by shortcut
+ Given I press "t"
+ Then I should see "find file" page
+
+ Scenario: Navigate to find file
+ Given I click Find File button
+ Then I should see "find file" page
+
+ @javascript
+ Scenario: I search file
+ Given I visit project find file page
+ And I fill in file find with "change"
+ Then I should not see ".gitignore" in files
+ And I should not see ".gitmodules" in files
+ And I should see "CHANGELOG" in files
+ And I should not see "VERSION" in files
+
+ @javascript
+ Scenario: I search file that not exist
+ Given I visit project find file page
+ And I fill in file find with "asdfghjklqwertyuizxcvbnm"
+ Then I should not see ".gitignore" in files
+ And I should not see ".gitmodules" in files
+ And I should not see "CHANGELOG" in files
+ And I should not see "VERSION" in files
+
+ @javascript
+ Scenario: I search file that partially matches
+ Given I visit project find file page
+ And I fill in file find with "git"
+ Then I should see ".gitignore" in files
+ And I should see ".gitmodules" in files
+ And I should not see "CHANGELOG" in files
+ And I should not see "VERSION" in files
diff --git a/features/project/fork.feature b/features/project/fork.feature
index 22f68e5b340..ca3f2771aa5 100644
--- a/features/project/fork.feature
+++ b/features/project/fork.feature
@@ -14,3 +14,36 @@ Feature: Project Fork
And I click link "Fork"
When I fork to my namespace
Then I should see a "Name has already been taken" warning
+
+ Scenario: Merge request on canonical repo goes to fork merge request page
+ Given I click link "Fork"
+ And I fork to my namespace
+ Then I should see the forked project page
+ When I visit project "Shop" page
+ Then I should see "New merge request"
+ And I goto the Merge Requests page
+ Then I should see "New merge request"
+ And I click link "New merge request"
+ Then I should see the new merge request page for my namespace
+
+ Scenario: Viewing forks of a Project
+ Given I click link "Fork"
+ When I fork to my namespace
+ And I visit the forks page of the "Shop" project
+ Then I should see my fork on the list
+
+ Scenario: Viewing forks of a Project that has no repo
+ Given I click link "Fork"
+ When I fork to my namespace
+ And I make forked repo invalid
+ And I visit the forks page of the "Shop" project
+ Then I should see my fork on the list
+
+ Scenario: Viewing private forks of a Project
+ Given There is an existent fork of the "Shop" project
+ And I click link "Fork"
+ When I fork to my namespace
+ And I visit the forks page of the "Shop" project
+ Then I should see my fork on the list
+ And I should not see the other fork listed
+ And I should see a private fork notice
diff --git a/features/project/group_links.feature b/features/project/group_links.feature
new file mode 100644
index 00000000000..2657c4487ad
--- /dev/null
+++ b/features/project/group_links.feature
@@ -0,0 +1,16 @@
+Feature: Project Group Links
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" is shared with group "Ops"
+ And project "Shop" is not shared with group "Market"
+ And I visit project group links page
+
+ Scenario: I should see list of groups
+ Then I should see project already shared with group "Ops"
+ Then I should see project is not shared with group "Market"
+
+ @javascript
+ Scenario: I share project with group
+ When I select group "Market" for share
+ Then I should see project is shared with group "Market"
diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature
index 9a06fdc2ee6..f0fd414a9f9 100644
--- a/features/project/issues/award_emoji.feature
+++ b/features/project/issues/award_emoji.feature
@@ -7,20 +7,35 @@ Feature: Award Emoji
And I visit "Bugfix" issue page
@javascript
- Scenario: I add and remove award in the issue
+ Scenario: I repeatedly add and remove thumbsup award in the issue
+ Given I click the thumbsup award Emoji
+ Then I have award added
+ Given I click the thumbsup award Emoji
+ Then I have no awards added
+ Given I click the thumbsup award Emoji
+ Then I have award added
+
+ @javascript
+ Scenario: I add and remove custom award in the issue
Given I click to emoji-picker
- And I click to emoji in the picker
+ Then The emoji menu is visible
+ And The search field is focused
+ Then I click to emoji in the picker
Then I have award added
And I can remove it by clicking to icon
@javascript
Scenario: I can see the list of emoji categories
Given I click to emoji-picker
+ Then The emoji menu is visible
+ And The search field is focused
Then I can see the activity and food categories
@javascript
Scenario: I can search emoji
Given I click to emoji-picker
+ Then The emoji menu is visible
+ And The search field is focused
And I search "hand"
Then I see search result for "hand"
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index ab234bc7507..ff21c7d1b83 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -25,9 +25,16 @@ Feature: Project Issues
Scenario: I visit issue page
Given I click link "Release 0.4"
Then I should see issue "Release 0.4"
+ And I should see "1 of 2" in the sidebar
+
+ Scenario: I navigate between issues
+ Given I click link "Release 0.4"
+ Then I click link "Next" in the sidebar
+ Then I should see issue "Tweet control"
+ And I should see "2 of 2" in the sidebar
@javascript
- Scenario: I visit issue page
+ Scenario: I filter by author
Given I add a user to project "Shop"
And I click "author" dropdown
Then I see current user as the first user
@@ -52,6 +59,38 @@ Feature: Project Issues
And I should see an error alert section within the comment form
@javascript
+ Scenario: Visiting Issues after being sorted the list
+ Given I visit project "Shop" issues page
+ And I sort the list by "Oldest updated"
+ And I visit my project's home page
+ And I visit project "Shop" issues page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Visiting Merge Requests after being sorted the list
+ Given I visit project "Shop" issues page
+ And I sort the list by "Oldest updated"
+ And I visit project "Shop" merge requests page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Visiting Merge Requests from a differente Project after sorting
+ Given I visit project "Shop" merge requests page
+ And I sort the list by "Oldest updated"
+ And I visit dashboard merge requests page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Sort issues by upvotes/downvotes
+ Given project "Shop" have "Bugfix" open issue
+ And issue "Release 0.4" have 2 upvotes and 1 downvote
+ And issue "Tweet control" have 1 upvote and 2 downvotes
+ And I sort the list by "Most popular"
+ Then The list should be sorted by "Most popular"
+ And I sort the list by "Least popular"
+ Then The list should be sorted by "Least popular"
+
+ @javascript
Scenario: I search issue
Given I fill in issue search with "Re"
Then I should see "Release 0.4" in issues
diff --git a/features/project/issues/references.feature b/features/project/issues/references.feature
new file mode 100644
index 00000000000..4ae2d653337
--- /dev/null
+++ b/features/project/issues/references.feature
@@ -0,0 +1,33 @@
+@project_issues
+Feature: Project Issues References
+ Background:
+ Given I sign in as "John Doe"
+ And public project "Community"
+ And "John Doe" owns public project "Community"
+ And project "Community" has "Community issue" open issue
+ And I logout
+ And I sign in as "Mary Jane"
+ And private project "Enterprise"
+ And "Mary Jane" owns private project "Enterprise"
+ And project "Enterprise" has "Enterprise issue" open issue
+ And project "Enterprise" has "Enterprise fix" open merge request
+ And I visit issue page "Enterprise issue"
+ And I leave a comment referencing issue "Community issue"
+ And I visit merge request page "Enterprise fix"
+ And I leave a comment referencing issue "Community issue"
+ And I logout
+
+ @javascript
+ Scenario: Viewing the public issue as a "John Doe"
+ Given I sign in as "John Doe"
+ When I visit issue page "Community issue"
+ Then I should not see any related merge requests
+ And I should see no notes at all
+
+ @javascript
+ Scenario: Viewing the public issue as "Mary Jane"
+ Given I sign in as "Mary Jane"
+ When I visit issue page "Community issue"
+ Then I should see the "Enterprise fix" related merge request
+ And I should see a note linking to "Enterprise fix" merge request
+ And I should see a note linking to "Enterprise issue" issue
diff --git a/features/project/labels.feature b/features/project/labels.feature
new file mode 100644
index 00000000000..955bc3d8b1b
--- /dev/null
+++ b/features/project/labels.feature
@@ -0,0 +1,15 @@
+@labels
+Feature: Labels
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" has labels: "bug", "feature", "enhancement"
+ When I visit project "Shop" labels page
+
+ @javascript
+ Scenario: I can subscribe to a label
+ Then I should see that I am not subscribed to the "bug" label
+ When I click button "Subscribe" for the "bug" label
+ Then I should see that I am subscribed to the "bug" label
+ When I click button "Unsubscribe" for the "bug" label
+ Then I should see that I am not subscribed to the "bug" label
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index aa9078b878f..74685d24a7d 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -26,6 +26,16 @@ Feature: Project Merge Requests
When I visit project "Shop" merge requests page
Then I should see "other_branch" branch
+ Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
+ Given project "Shop" have "Bug NS-07" open merge request with rebased branch
+ When I visit merge request page "Bug NS-07"
+ Then I should not see the diverged commits count
+
+ Scenario: I should see the numbers of diverged commits if the branch diverged from the target
+ Given project "Shop" have "Bug NS-08" open merge request with diverged branch
+ When I visit merge request page "Bug NS-08"
+ Then I should see the diverged commits count
+
Scenario: I should see rejected merge requests
Given I click link "Closed"
Then I should see "Feature NS-03" in merge requests
@@ -36,9 +46,17 @@ Feature: Project Merge Requests
Then I should see "Feature NS-03" in merge requests
And I should see "Bug NS-04" in merge requests
- Scenario: I visit merge request page
+ Scenario: I visit an open merge request page
Given I click link "Bug NS-04"
Then I should see merge request "Bug NS-04"
+ And I should see "1 of 1" in the sidebar
+
+ Scenario: I visit a merged merge request page
+ Given project "Shop" have "Feature NS-05" merged merge request
+ And I click link "Merged"
+ And I click link "Feature NS-05"
+ Then I should see merge request "Feature NS-05"
+ And I should see "3 of 3" in the sidebar
Scenario: I close merge request page
Given I click link "Bug NS-04"
@@ -76,6 +94,39 @@ Feature: Project Merge Requests
Then I should see comment "XML attached"
@javascript
+ Scenario: Visiting Merge Requests after being sorted the list
+ Given I visit project "Shop" merge requests page
+ And I sort the list by "Oldest updated"
+ And I visit my project's home page
+ And I visit project "Shop" merge requests page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Visiting Issues after being sorted the list
+ Given I visit project "Shop" merge requests page
+ And I sort the list by "Oldest updated"
+ And I visit project "Shop" issues page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Visiting Merge Requests from a differente Project after sorting
+ Given I visit project "Shop" merge requests page
+ And I sort the list by "Oldest updated"
+ And I visit dashboard merge requests page
+ Then The list should be sorted by "Oldest updated"
+
+ @javascript
+ Scenario: Sort merge requests by upvotes/downvotes
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ And project "Shop" have "Bug NS-06" open merge request
+ And merge request "Bug NS-04" have 2 upvotes and 1 downvote
+ And merge request "Bug NS-06" have 1 upvote and 2 downvotes
+ And I sort the list by "Most popular"
+ Then The list should be sorted by "Most popular"
+ And I sort the list by "Least popular"
+ Then The list should be sorted by "Least popular"
+
+ @javascript
Scenario: I comment on a merge request diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
@@ -83,6 +134,15 @@ Feature: Project Merge Requests
And I leave a comment like "Line is wrong" on diff
And I switch to the merge request's comments tab
Then I should see a discussion has started on diff
+ And I should see a badge of "1" next to the discussion link
+
+ @javascript
+ Scenario: I see a new comment on merge request diff from another user in the discussion tab
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ And I visit merge request page "Bug NS-05"
+ And user "John Doe" leaves a comment like "Line is wrong" on diff
+ Then I should see a discussion by user "John Doe" has started on diff
+ And I should see a badge of "1" next to the discussion link
@javascript
Scenario: I edit a comment on a merge request diff
@@ -100,9 +160,11 @@ Feature: Project Merge Requests
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is wrong" on diff
+ And I should see a badge of "1" next to the discussion link
And I delete the comment "Line is wrong" on diff
And I click on the Discussion tab
Then I should not see any discussion
+ And I should see a badge of "0" next to the discussion link
@javascript
Scenario: I comment on a line of a commit in merge request
diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature
new file mode 100644
index 00000000000..571612261a9
--- /dev/null
+++ b/features/project/merge_requests/references.feature
@@ -0,0 +1,31 @@
+@project_merge_requests
+Feature: Project Merge Requests References
+ Background:
+ Given I sign in as "John Doe"
+ And public project "Community"
+ And "John Doe" owns public project "Community"
+ And project "Community" has "Community fix" open merge request
+ And I logout
+ And I sign in as "Mary Jane"
+ And private project "Enterprise"
+ And "Mary Jane" owns private project "Enterprise"
+ And project "Enterprise" has "Enterprise issue" open issue
+ And project "Enterprise" has "Enterprise fix" open merge request
+ And I visit issue page "Enterprise issue"
+ And I leave a comment referencing issue "Community fix"
+ And I visit merge request page "Enterprise fix"
+ And I leave a comment referencing issue "Community fix"
+ And I logout
+
+ @javascript
+ Scenario: Viewing the public issue as a "John Doe"
+ Given I sign in as "John Doe"
+ When I visit issue page "Community fix"
+ Then I should see no notes at all
+
+ @javascript
+ Scenario: Viewing the public issue as "Mary Jane"
+ Given I sign in as "Mary Jane"
+ When I visit issue page "Community fix"
+ And I should see a note linking to "Enterprise fix" merge request
+ And I should see a note linking to "Enterprise issue" issue
diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature
new file mode 100644
index 00000000000..d767b088883
--- /dev/null
+++ b/features/project/merge_requests/revert.feature
@@ -0,0 +1,30 @@
+@project_merge_requests
+Feature: Revert Merge Requests
+ Background:
+ Given There is an open Merge Request
+ And I am signed in as a developer of the project
+ And I am on the Merge Request detail page
+ And I click on Accept Merge Request
+
+ @javascript
+ Scenario: I revert a merge request
+ Given I click on the revert button
+ And I revert the changes directly
+ Then I should see the revert merge request notice
+
+ @javascript
+ Scenario: I revert a merge request that was previously reverted
+ Given I click on the revert button
+ And I revert the changes directly
+ And I am on the Merge Request detail page
+ And I click on the revert button
+ And I revert the changes directly
+ Then I should see a revert error
+
+ @javascript
+ Scenario: I revert a merge request in a new merge request
+ Given I click on the revert button
+ And I am on the Merge Request detail page
+ And I click on the revert button
+ And I revert the changes in a new merge request
+ Then I should see the new merge request notice
diff --git a/features/project/milestone.feature b/features/project/milestone.feature
new file mode 100644
index 00000000000..713f0f3b979
--- /dev/null
+++ b/features/project/milestone.feature
@@ -0,0 +1,24 @@
+Feature: Project Milestone
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" has labels: "bug", "feature", "enhancement"
+ And project "Shop" has milestone "v2.2"
+ And milestone has issue "Bugfix1" with labels: "bug", "feature"
+ And milestone has issue "Bugfix2" with labels: "bug", "enhancement"
+
+
+ @javascript
+ Scenario: Listing issues from issues tab
+ Given I visit project "Shop" milestones page
+ And I click link "v2.2"
+ Then I should see the labels "bug", "enhancement" and "feature"
+ And I should see the "bug" label listed only once
+
+ @javascript
+ Scenario: Listing labels from labels tab
+ Given I visit project "Shop" milestones page
+ And I click link "v2.2"
+ And I click link "Labels"
+ Then I should see the list of labels
+ And I should see the labels "bug", "enhancement" and "feature"
diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature
index 6cc89a15a78..89a02706bd2 100644
--- a/features/project/network_graph.feature
+++ b/features/project/network_graph.feature
@@ -34,9 +34,10 @@ Feature: Project Network Graph
@javascript
Scenario: I should filter selected tag
When I switch ref to "v1.0.0"
+ Then page should have "v1.0.0" in title
Then page should have content not containing "v1.0.0"
When click "Show only selected branch" checkbox
- Then page should not have content not containing "v1.0.0"
+ Then page should only have content from "v1.0.0"
When click "Show only selected branch" checkbox
Then page should have content not containing "v1.0.0"
diff --git a/features/project/project.feature b/features/project/project.feature
index 1a53945eb04..f1f3ed26065 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -86,3 +86,9 @@ Feature: Project
Given I click notifications drop down button
When I choose Mention setting
Then I should see Notification saved message
+
+ Scenario: I should see command line instructions
+ Given I own an empty project
+ And I visit my empty project page
+ And I create bare repo
+ Then I should see command line instructions
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index a8c276b949e..1e09dbc4c8f 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -320,3 +320,13 @@ Feature: Project Source Browse Files
Then I should see download link and object size
And I should not see lfs pointer details
And I should see buttons for allowed commands
+
+ @javascript
+ Scenario: I preview an SVG file
+ Given I click on "Upload file" link in repo
+ And I upload a new SVG file
+ And I fill the upload file commit message
+ And I fill the new branch name
+ And I click on "Upload file"
+ Given I visit the SVG file
+ Then I can see the new rendered SVG image
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
index 06fb45c8bde..5888662fc3f 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -39,3 +39,8 @@ Feature: Project Team Management
And I click link "Import team from another project"
And I submit "Website" project for import team
Then I should see "Mike" in team list as "Reporter"
+
+ Scenario: See all members of projects shared group
+ Given I share project with group "OpenSource"
+ And I visit project "Shop" team page
+ Then I should see "Opensource" group user listing
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index af970ecf2d0..d4811b1ff54 100644
--- a/features/project/wiki.feature
+++ b/features/project/wiki.feature
@@ -70,11 +70,6 @@ Feature: Project Wiki
Then I should see non-escaped link in the pages list
@javascript
- Scenario: Creating an invalid new page
- Given I create a New page with an invalid name
- Then I should see an error message
-
- @javascript
Scenario: Edit Wiki page that has a path
Given I create a New page with paths
And I click on the "Pages" button
diff --git a/features/search.feature b/features/search.feature
index a9234c1a611..3cd52810e59 100644
--- a/features/search.feature
+++ b/features/search.feature
@@ -65,3 +65,25 @@ Feature: Search
And I search for "Wiki content"
And I click "Wiki" link
Then I should see "test_wiki" link in the search results
+
+ Scenario: I logout and should see project I am looking for
+ Given project "Shop" is public
+ And I logout
+ And I search for "Sho"
+ Then I should see "Shop" project link
+
+ Scenario: I logout and should see issues I am looking for
+ Given project "Shop" is public
+ And I logout
+ And project has issues
+ When I search for "Foo"
+ And I click "Issues" link
+ Then I should see "Foo" link in the search results
+ And I should not see "Bar" link in the search results
+
+ Scenario: I logout and should see project code I am looking for
+ Given project "Shop" is public
+ And I logout
+ When I visit project "Shop" page
+ And I search for "rspec" on project page
+ Then I should see code results for project "Shop"
diff --git a/features/steps/admin/appearance.rb b/features/steps/admin/appearance.rb
new file mode 100644
index 00000000000..0d1be46d11d
--- /dev/null
+++ b/features/steps/admin/appearance.rb
@@ -0,0 +1,72 @@
+class Spinach::Features::AdminAppearance < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+
+ step 'submit form with new appearance' do
+ fill_in 'appearance_title', with: 'MyCompany'
+ fill_in 'appearance_description', with: 'dev server'
+ click_button 'Save'
+ end
+
+ step 'I should be redirected to admin appearance page' do
+ expect(current_path).to eq admin_appearances_path
+ expect(page).to have_content 'Appearance settings'
+ end
+
+ step 'I should see newly created appearance' do
+ expect(page).to have_field('appearance_title', with: 'MyCompany')
+ expect(page).to have_field('appearance_description', with: 'dev server')
+ expect(page).to have_content 'Last edit'
+ end
+
+ step 'I click preview button' do
+ click_link "Preview"
+ end
+
+ step 'application has custom appearance' do
+ create(:appearance)
+ end
+
+ step 'I should see a customized appearance' do
+ expect(page).to have_content appearance.title
+ expect(page).to have_content appearance.description
+ end
+
+ step 'I attach a logo' do
+ attach_file(:appearance_logo, Rails.root.join('spec', 'fixtures', 'dk.png'))
+ click_button 'Save'
+ end
+
+ step 'I attach header logos' do
+ attach_file(:appearance_header_logo, Rails.root.join('spec', 'fixtures', 'dk.png'))
+ click_button 'Save'
+ end
+
+ step 'I should see a logo' do
+ expect(page).to have_xpath('//img[@src="/uploads/appearance/logo/1/dk.png"]')
+ end
+
+ step 'I should see header logos' do
+ expect(page).to have_xpath('//img[@src="/uploads/appearance/header_logo/1/dk.png"]')
+ end
+
+ step 'I remove the logo' do
+ click_link 'Remove logo'
+ end
+
+ step 'I remove the header logos' do
+ click_link 'Remove header logo'
+ end
+
+ step 'I should see logo removed' do
+ expect(page).not_to have_xpath('//img[@src="/uploads/appearance/logo/1/gitlab_logo.png"]')
+ end
+
+ step 'I should see header logos removed' do
+ expect(page).not_to have_xpath('//img[@src="/uploads/appearance/header_logo/1/header_logo_light.png"]')
+ end
+
+ def appearance
+ Appearance.last
+ end
+end
diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb
index f6daf852977..af2b4a29313 100644
--- a/features/steps/admin/broadcast_messages.rb
+++ b/features/steps/admin/broadcast_messages.rb
@@ -1,22 +1,15 @@
class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
- include SharedAdmin
- step 'application already has admin messages' do
- FactoryGirl.create(:broadcast_message, message: "Migration to new server")
+ step 'application already has a broadcast message' do
+ FactoryGirl.create(:broadcast_message, :expired, message: "Migration to new server")
end
- step 'I should be all broadcast messages' do
+ step 'I should see all broadcast messages' do
expect(page).to have_content "Migration to new server"
end
- step 'submit form with new broadcast message' do
- fill_in 'broadcast_message_message', with: 'Application update from 4:00 CST to 5:00 CST'
- select '2018', from: "broadcast_message_ends_at_1i"
- click_button "Add broadcast message"
- end
-
step 'I should be redirected to admin messages page' do
expect(current_path).to eq admin_broadcast_messages_path
end
@@ -26,16 +19,48 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps
end
step 'submit form with new customized broadcast message' do
- fill_in 'broadcast_message_message', with: 'Application update from 4:00 CST to 5:00 CST'
- click_link "Customize colors"
+ fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**'
fill_in 'broadcast_message_color', with: '#f2dede'
fill_in 'broadcast_message_font', with: '#b94a48'
- select '2018', from: "broadcast_message_ends_at_1i"
+ select Date.today.next_year.year, from: "broadcast_message_ends_at_1i"
click_button "Add broadcast message"
end
step 'I should see a customized broadcast message' do
expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST'
+ expect(page).to have_selector 'strong', text: '4:00 CST to 5:00 CST'
expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"])
end
+
+ step 'I edit an existing broadcast message' do
+ click_link 'Edit'
+ end
+
+ step 'I change the broadcast message text' do
+ fill_in 'broadcast_message_message', with: 'Application update RIGHT NOW'
+ click_button 'Update broadcast message'
+ end
+
+ step 'I should see the updated broadcast message' do
+ expect(page).to have_content "Application update RIGHT NOW"
+ end
+
+ step 'I remove an existing broadcast message' do
+ click_link 'Remove'
+ end
+
+ step 'I should not see the removed broadcast message' do
+ expect(page).not_to have_content 'Migration to new server'
+ end
+
+ step 'I enter a broadcast message with Markdown' do
+ fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:"
+ end
+
+ step 'I should see a live preview of the rendered broadcast message' do
+ page.within('.broadcast-message-preview') do
+ expect(page).to have_selector('strong', text: 'Markdown')
+ expect(page).to have_selector('img.emoji')
+ end
+ end
end
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index 43fd91d0d4c..e1f1db2872f 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -73,6 +73,21 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
end
end
+ step 'group has shared projects' do
+ share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = current_group.id
+ share_link.save!
+ end
+
+ step 'I visit group page' do
+ visit admin_group_path(current_group)
+ end
+
+ step 'I should see project shared with group' do
+ expect(page).to have_content(shared_project.name_with_namespace)
+ expect(page).to have_content "Projects shared with"
+ end
+
step 'we have user "John Doe" in group' do
current_group.add_reporter(user_john)
end
@@ -123,6 +138,10 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
@group ||= Group.first
end
+ def shared_project
+ @shared_project ||= create(:empty_project)
+ end
+
def user_john
@user_john ||= User.find_by(name: "John Doe")
end
diff --git a/features/steps/admin/spam_logs.rb b/features/steps/admin/spam_logs.rb
new file mode 100644
index 00000000000..ad825fd414c
--- /dev/null
+++ b/features/steps/admin/spam_logs.rb
@@ -0,0 +1,28 @@
+class Spinach::Features::AdminSpamLogs < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedAdmin
+
+ step 'I should see list of spam logs' do
+ expect(page).to have_content('Spam Logs')
+ expect(page).to have_content spam_log.source_ip
+ expect(page).to have_content spam_log.noteable_type
+ expect(page).to have_content 'N'
+ expect(page).to have_content spam_log.title
+ expect(page).to have_content truncate(spam_log.description)
+ expect(page).to have_link('Remove user')
+ expect(page).to have_link('Block user')
+ end
+
+ step 'spam logs exist' do
+ create(:spam_log)
+ end
+
+ def spam_log
+ @spam_log ||= SpamLog.first
+ end
+
+ def truncate(description)
+ "#{spam_log.description[0...97]}..."
+ end
+end
diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb
index 36e092f50c6..6510f8d9b32 100644
--- a/features/steps/dashboard/archived_projects.rb
+++ b/features/steps/dashboard/archived_projects.rb
@@ -19,4 +19,8 @@ class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps
step 'I should see "Forum" project link' do
expect(page).to have_link "Forum"
end
+
+ step 'I click "Show archived projects" link' do
+ click_link "Show archived projects"
+ end
end
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index 63f0ec2b6e8..5062e348844 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -2,6 +2,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
+ include SharedIssuable
step 'I should see "New Project" link' do
expect(page).to have_link "New project"
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index cbe54e2dc79..f4a56865532 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -36,13 +36,17 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- select2(current_user.id, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ find("#assignee_id").set("")
+ find(".js-author-search", match: :first).click
+ find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
end
step 'I click "All" link' do
- select2(nil, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ 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
end
def should_see(issue)
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 28c8c6b6015..a2adc87f8ef 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -40,13 +40,16 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- select2(current_user.id, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ find("#assignee_id").set("")
+ find(".js-author-search", match: :first).click
+ find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
end
step 'I click "All" link' do
- select2(nil, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ 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(merge_request)
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
new file mode 100644
index 00000000000..963e4f21365
--- /dev/null
+++ b/features/steps/dashboard/todos.rb
@@ -0,0 +1,127 @@
+class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+ include SharedUser
+ include Select2Helper
+
+ step '"John Doe" is a developer of project "Shop"' do
+ project.team << [john_doe, :developer]
+ end
+
+ step 'I am a developer of project "Enterprise"' do
+ enterprise.team << [current_user, :developer]
+ end
+
+ step '"Mary Jane" is a developer of project "Shop"' do
+ project.team << [john_doe, :developer]
+ end
+
+ step 'I have todos' do
+ create(:todo, user: current_user, project: project, author: mary_jane, target: issue, action: Todo::MENTIONED)
+ create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::ASSIGNED)
+ note = create(:note, author: john_doe, noteable: issue, note: "#{current_user.to_reference} Wdyt?")
+ create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::MENTIONED, note: note)
+ create(:todo, user: current_user, project: project, author: john_doe, target: merge_request, action: Todo::ASSIGNED)
+ end
+
+ step 'I should see todos assigned to me' do
+ expect(page).to have_content 'To do 4'
+ expect(page).to have_content 'Done 0'
+
+ expect(page).to have_link project.name_with_namespace
+ should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title)
+ should_see_todo(2, "John Doe mentioned you on issue ##{issue.iid}", "#{current_user.to_reference} Wdyt?")
+ should_see_todo(3, "John Doe assigned you issue ##{issue.iid}", issue.title)
+ should_see_todo(4, "Mary Jane mentioned you on issue ##{issue.iid}", issue.title)
+ end
+
+ step 'I mark the todo as done' do
+ page.within('.todo:nth-child(1)') do
+ click_link 'Done'
+ end
+
+ 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}"
+ end
+
+ step 'I click on the "Done" tab' do
+ click_link 'Done 1'
+ end
+
+ step 'I should see all todos marked as done' do
+ expect(page).to have_link project.name_with_namespace
+ should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title, false)
+ end
+
+ step 'I filter by "Enterprise"' do
+ select2(enterprise.id, from: "#project_id")
+ end
+
+ step 'I filter by "John Doe"' do
+ select2(john_doe.id, from: "#author_id")
+ end
+
+ step 'I filter by "Issue"' do
+ select2('Issue', from: "#type")
+ end
+
+ step 'I filter by "Mentioned"' do
+ select2("#{Todo::MENTIONED}", from: '#action_id')
+ end
+
+ step 'I should not see todos' do
+ expect(page).to have_content "You're all done!"
+ end
+
+ step 'I should not see todos related to "Mary Jane" in the list' do
+ should_not_see_todo "Mary Jane mentioned you on issue ##{issue.iid}"
+ end
+
+ step 'I should not see todos related to "Merge Requests" in the list' do
+ should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
+ end
+
+ step 'I should not see todos related to "Assignments" in the list' do
+ should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
+ should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
+ end
+
+ def should_see_todo(position, title, body, pending = true)
+ page.within(".todo:nth-child(#{position})") do
+ expect(page).to have_content title
+ expect(page).to have_content body
+
+ if pending
+ expect(page).to have_link 'Done'
+ else
+ expect(page).to_not have_link 'Done'
+ end
+ end
+ end
+
+ def should_not_see_todo(title)
+ expect(page).not_to have_content title
+ end
+
+ def john_doe
+ @john_doe ||= user_exists("John Doe", { username: "john_doe" })
+ end
+
+ def mary_jane
+ @mary_jane ||= user_exists("Mary Jane", { username: "mary_jane" })
+ end
+
+ def enterprise
+ @enterprise ||= Project.find_by(name: 'Enterprise')
+ end
+
+ def issue
+ @issue ||= create(:issue, assignee: current_user, project: project)
+ end
+
+ def merge_request
+ @merge_request ||= create(:merge_request, assignee: current_user, source_project: project)
+ end
+end
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index 742ba5d71f6..cb6fa8a47da 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -18,7 +18,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
end
step 'I should see empty public project details' do
- expect(page).to have_content 'Git global setup'
+ expect(page).not_to have_content 'Git global setup'
end
step 'I should see empty public project details with http clone info' do
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index 6e57b16ccb6..a167d259837 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -24,16 +24,19 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end
step 'I click on one group milestone' do
+ milestones = Milestone.where(title: 'GL-113')
+ @global_milestone = GlobalMilestone.new('GL-113', milestones)
+
click_link 'GL-113'
end
step 'I should see group milestone with descriptions and expiry date' do
- expect(page).to have_content('expires at Aug 20, 2114')
+ expect(page).to have_content('expires on Aug 20, 2114')
end
step 'I should see group milestone with all issues and MRs assigned to that milestone' do
expect(page).to have_content('Milestone GL-113')
- expect(page).to have_content('Progress: 0 closed – 3 open')
+ expect(page).to have_content('3 issues: 3 open and 0 closed')
issue = Milestone.find_by(name: 'GL-113').issues.first
expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue))
end
@@ -60,6 +63,39 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end
end
+ step 'I should see the "bug" label' do
+ page.within('#tab-issues') do
+ expect(page).to have_content 'bug'
+ end
+ end
+
+ step 'I should see the "feature" label' do
+ page.within('#tab-issues') do
+ expect(page).to have_content 'bug'
+ end
+ end
+
+ step 'I should see the project name in the Issue row' do
+ page.within('#tab-issues') do
+ @global_milestone.projects.each do |project|
+ expect(page).to have_content project.name
+ end
+ end
+ end
+
+ step 'I click on the "Labels" tab' do
+ page.within('.nav-links') do
+ page.find(:xpath, "//a[@href='#tab-labels']").click
+ end
+ end
+
+ step 'I should see the list of labels' do
+ page.within('#tab-labels') do
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'feature'
+ end
+ end
+
private
def group_milestone
@@ -68,6 +104,10 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
project = create :project, path: path, group: group
milestone = create :milestone, title: "Version 7.2", project: project
+
+ create(:label, project: project, title: 'bug')
+ create(:label, project: project, title: 'feature')
+
create :issue,
project: project,
assignee: current_user,
@@ -80,11 +120,14 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
due_date: '2114-08-20',
description: 'Lorem Ipsum is simply dummy text'
- create :issue,
+ issue = create :issue,
project: project,
assignee: current_user,
author: current_user,
milestone: milestone
+
+ issue.labels << project.labels.find_by(title: 'bug')
+ issue.labels << project.labels.find_by(title: 'feature')
end
end
end
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 4c5122d1b7d..e5b7db4c5e3 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -35,7 +35,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
@@ -44,6 +44,18 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
end
+ step 'I should not see issues from the archived project' do
+ @archived_project.issues.each do |issue|
+ expect(page).not_to have_content issue.title
+ end
+ end
+
+ step 'I should not see merge requests from the archived project' do
+ @archived_project.merge_requests.each do |mr|
+ expect(page).not_to have_content mr.title
+ end
+ end
+
step 'I should see merge requests from group "Owned" assigned to me' do
assigned_to_me(:merge_requests).each do |issue|
expect(page).to have_content issue.title[0..80]
@@ -113,13 +125,32 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'Group "Owned" has archived project' do
group = Group.find_by(name: 'Owned')
- create(:project, namespace: group, archived: true, path: "archived-project")
+ @archived_project = create(:project, namespace: group, archived: true, path: "archived-project")
end
step 'I should see "archived" label' do
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
+ step 'I visit group "NonExistentGroup" page' do
+ visit group_path(-1)
+ end
+
+ step 'the archived project have some issues' do
+ create :issue,
+ project: @archived_project,
+ assignee: current_user,
+ author: current_user
+ end
+
+ step 'the archived project have some merge requests' do
+ create :merge_request,
+ source_project: @archived_project,
+ target_project: @archived_project,
+ assignee: current_user,
+ author: current_user
+ end
+
private
def assigned_to_me(key)
diff --git a/features/steps/login_form.rb b/features/steps/login_form.rb
deleted file mode 100644
index b9ff6ae67fd..00000000000
--- a/features/steps/login_form.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class Spinach::Features::LoginForm < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
- include SharedUser
- include SharedSearch
-
- step 'Crowd integration enabled' do
- @providers_orig = Gitlab::OAuth::Provider.providers
- @omniauth_conf_orig = Gitlab.config.omniauth.enabled
- expect(Gitlab::OAuth::Provider).to receive(:providers).and_return([:crowd])
- allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path)
- expect(Gitlab.config.omniauth).to receive(:enabled).and_return(true)
- end
-
- step 'I should see Crowd login form' do
- expect(page).to have_selector '#tab-crowd form'
- Gitlab::OAuth::Provider.stub(:providers).and_return(@providers_orig)
- Gitlab.config.omniauth.stub(:enabled).and_return(@omniauth_conf_orig)
- end
-
- step 'I visit sign in page' do
- visit new_user_session_path
- end
-end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 0305f7e6da0..909de31a479 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -13,7 +13,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
fill_in 'user_website_url', with: 'testurl'
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
- click_button 'Save changes'
+ click_button 'Update profile settings'
@user.reload
end
@@ -28,7 +28,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I change my avatar' do
attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
- click_button "Save changes"
+ click_button "Update profile settings"
@user.reload
end
@@ -43,7 +43,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I have an avatar' do
attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
- click_button "Save changes"
+ click_button "Update profile settings"
@user.reload
end
@@ -68,7 +68,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
page.within '.update-password' do
fill_in "user_password", with: "22233344"
fill_in "user_password_confirmation", with: "22233344"
- click_button "Save"
+ click_button "Save password"
end
end
@@ -77,7 +77,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
fill_in "user_current_password", with: "12345678"
fill_in "user_password", with: "22233344"
fill_in "user_password_confirmation", with: "22233344"
- click_button "Save"
+ click_button "Save password"
end
end
@@ -86,7 +86,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
fill_in "user_current_password", with: "12345678"
fill_in "user_password", with: "password"
fill_in "user_password_confirmation", with: "confirmation"
- click_button "Save"
+ click_button "Save password"
end
end
@@ -97,15 +97,15 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step "I should see a password error message" do
- page.within '.alert' do
+ page.within '.alert-danger' do
expect(page).to have_content "Password confirmation doesn't match"
end
end
step 'I reset my token' do
- page.within '.update-token' do
+ page.within '.private-token' do
@old_token = @user.private_token
- click_button "Reset"
+ click_button "Reset private token"
end
end
@@ -184,18 +184,14 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
end
- step 'I click on new application button' do
- click_on 'New Application'
- end
-
step 'I should see application form' do
- expect(page).to have_content "New Application"
+ expect(page).to have_content "Add new application"
end
step 'I fill application form out and submit' do
fill_in :doorkeeper_application_name, with: 'test'
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
- click_on "Submit"
+ click_on "Save application"
end
step 'I see application' do
@@ -215,7 +211,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I change name of application and submit' do
expect(page).to have_content "Edit application"
fill_in :doorkeeper_application_name, with: 'test_changed'
- click_on "Submit"
+ click_on "Save application"
end
step 'I see that application was changed' do
diff --git a/features/steps/profile/ssh_keys.rb b/features/steps/profile/ssh_keys.rb
index c7f879d247d..a400488a532 100644
--- a/features/steps/profile/ssh_keys.rb
+++ b/features/steps/profile/ssh_keys.rb
@@ -7,8 +7,8 @@ class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps
end
end
- step 'I click link "Add new"' do
- click_link "Add SSH Key"
+ step 'I should see new ssh key form' do
+ expect(page).to have_content("Add an SSH key")
end
step 'I submit new ssh key "Laptop"' do
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 9e96fa5ba49..19d81453d8c 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -26,7 +26,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Hooks" tab' do
- click_link('Web Hooks')
+ click_link('Webhooks')
end
step 'I click the "Deploy Keys" tab' do
@@ -42,7 +42,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'the active sub nav should be Hooks' do
- ensure_active_sub_nav('Web Hooks')
+ ensure_active_sub_nav('Webhooks')
end
step 'the active sub nav should be Deploy Keys' do
diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb
new file mode 100644
index 00000000000..66a48a176e5
--- /dev/null
+++ b/features/steps/project/badges/build.rb
@@ -0,0 +1,32 @@
+class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedBuilds
+ include RepoHelpers
+
+ step 'I display builds badge for a master branch' do
+ visit build_namespace_project_badges_path(@project.namespace, @project, ref: :master, format: :svg)
+ end
+
+ step 'I should see a build success badge' do
+ expect_badge('success')
+ end
+
+ step 'I should see a build failed badge' do
+ expect_badge('failed')
+ end
+
+ step 'I should see a build running badge' do
+ expect_badge('running')
+ end
+
+ step 'I should see a badge that has not been cached' do
+ expect(page.response_headers['Cache-Control']).to include 'no-cache'
+ end
+
+ def expect_badge(status)
+ svg = Nokogiri::XML.parse(page.body)
+ expect(page.response_headers).to include('Content-Type' => 'image/svg+xml')
+ expect(svg.at(%Q{text:contains("#{status}")})).to be_truthy
+ end
+end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
new file mode 100644
index 00000000000..1bdb57af9d1
--- /dev/null
+++ b/features/steps/project/builds/artifacts.rb
@@ -0,0 +1,86 @@
+class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedBuilds
+ include RepoHelpers
+
+ step 'I click artifacts download button' do
+ page.within('.artifacts') { click_link 'Download' }
+ end
+
+ step 'I click artifacts browse button' do
+ page.within('.artifacts') { click_link 'Browse' }
+ end
+
+ step 'I should see content of artifacts archive' do
+ page.within('.tree-table') do
+ expect(page).to have_no_content '..'
+ expect(page).to have_content 'other_artifacts_0.1.2'
+ expect(page).to have_content 'ci_artifacts.txt'
+ expect(page).to have_content 'rails_sample.jpg'
+ end
+ end
+
+ step 'I click link to subdirectory within build artifacts' do
+ page.within('.tree-table') { click_link 'other_artifacts_0.1.2' }
+ end
+
+ step 'I should see content of subdirectory within artifacts archive' do
+ page.within('.tree-table') do
+ expect(page).to have_content '..'
+ expect(page).to have_content 'another-subdirectory'
+ expect(page).to have_content 'doc_sample.txt'
+ end
+ end
+
+ step 'recent build artifacts contain directory with UTF-8 characters' do
+ # metadata fixture contains relevant directory
+ end
+
+ step 'I navigate to directory with UTF-8 characters in name' do
+ page.within('.tree-table') { click_link 'tests_encoding' }
+ page.within('.tree-table') { click_link 'utf8 test dir ✓' }
+ end
+
+ step 'I should see content of directory with UTF-8 characters in name' do
+ page.within('.tree-table') do
+ expect(page).to have_content '..'
+ expect(page).to have_content 'regular_file_2'
+ end
+ end
+
+ step 'recent build artifacts contain directory with invalid UTF-8 characters' do
+ # metadata fixture contains relevant directory
+ end
+
+ step 'I navigate to parent directory of directory with invalid name' do
+ page.within('.tree-table') { click_link 'tests_encoding' }
+ end
+
+ step 'I should not see directory with invalid name on the list' do
+ page.within('.tree-table') do
+ expect(page).to have_no_content('non-utf8-dir')
+ end
+ end
+
+ step 'I click a link to file within build artifacts' do
+ page.within('.tree-table') { find_link('ci_artifacts.txt').click }
+ end
+
+ step 'download of a file extracted from build artifacts should start' do
+ # this will be accelerated by Workhorse
+ response_json = JSON.parse(page.body, symbolize_names: true)
+ expect(response_json[:archive]).to end_with('build_artifacts.zip')
+ expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt')
+ end
+
+ step 'I click a first row within build artifacts table' do
+ row = first('tr[data-link]')
+ @row_path = row['data-link']
+ row.click
+ end
+
+ step 'page with a coresponding path is loading' do
+ expect(current_path).to eq @row_path
+ end
+end
diff --git a/features/steps/project/builds/permissions.rb b/features/steps/project/builds/permissions.rb
new file mode 100644
index 00000000000..6e9d6504fd5
--- /dev/null
+++ b/features/steps/project/builds/permissions.rb
@@ -0,0 +1,7 @@
+class Spinach::Features::ProjectBuildsPermissions < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedBuilds
+ include SharedPaths
+ include RepoHelpers
+end
diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb
new file mode 100644
index 00000000000..e9e2359146e
--- /dev/null
+++ b/features/steps/project/builds/summary.rb
@@ -0,0 +1,39 @@
+class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedBuilds
+ include RepoHelpers
+
+ step 'I see coverage' do
+ page.within('td.coverage') do
+ expect(page).to have_content "99.9%"
+ end
+ end
+
+ step 'I see button to CI Lint' do
+ page.within('.nav-controls') do
+ ci_lint_tool_link = page.find_link('CI Lint')
+ expect(ci_lint_tool_link[:href]).to eq ci_lint_path
+ end
+ end
+
+ step 'I click erase build button' do
+ click_link 'Erase'
+ end
+
+ step 'recent build has been erased' do
+ expect(@build.artifacts_file.exists?).to be_falsy
+ expect(@build.artifacts_metadata.exists?).to be_falsy
+ expect(@build.trace).to be_empty
+ end
+
+ step 'recent build summary does not have artifacts widget' do
+ expect(page).to have_no_css('.artifacts')
+ end
+
+ step 'recent build summary contains information saying that build has been erased' do
+ page.within('.erased') do
+ expect(page).to have_content 'Build has been erased'
+ end
+ end
+end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index a3141fe3be1..93c37bf507f 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -33,6 +33,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "Showing #{sample_commit.files_changed_count} changed files"
end
+ step 'I fill compare fields with branches' do
+ fill_in 'from', with: 'feature'
+ fill_in 'to', with: 'master'
+
+ click_button 'Compare'
+ end
+
step 'I fill compare fields with refs' do
fill_in "from", with: sample_commit.parent_id
fill_in "to", with: sample_commit.id
@@ -56,6 +63,56 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "Showing 2 changed files"
end
+ step 'I visit commits list page for feature branch' do
+ visit namespace_project_commits_path(@project.namespace, @project, 'feature', { limit: 5 })
+ end
+
+ step 'I see feature branch commits' do
+ commit = @project.repository.commit('0b4bc9a')
+ expect(page).to have_content(@project.name)
+ expect(page).to have_content(commit.message[0..12])
+ expect(page).to have_content(commit.short_id)
+ end
+
+ step 'project have an open merge request' do
+ create(:merge_request,
+ title: 'Feature',
+ source_project: @project,
+ source_branch: 'feature',
+ target_branch: 'master',
+ author: @project.users.first
+ )
+ end
+
+ step 'I click the "Compare" tab' do
+ click_link('Compare')
+ end
+
+ step 'I fill compare fields with branches' do
+ fill_in 'from', with: 'master'
+ fill_in 'to', with: 'feature'
+
+ click_button 'Compare'
+ end
+
+ step 'I see compared branches' do
+ expect(page).to have_content 'Commits (1)'
+ expect(page).to have_content 'Showing 1 changed file with 5 additions and 0 deletions'
+ end
+
+ step 'I see button to create a new merge request' do
+ expect(page).to have_link 'Create Merge Request'
+ end
+
+ step 'I should not see button to create a new merge request' do
+ expect(page).to_not have_link 'Create Merge Request'
+ end
+
+ step 'I should see button to the merge request' do
+ merge_request = MergeRequest.find_by(title: 'Feature')
+ expect(page).to have_link "View Open Merge Request", href: namespace_project_merge_request_path(@project.namespace, @project, merge_request)
+ end
+
step 'I see breadcrumb links' do
expect(page).to have_selector('ul.breadcrumb')
expect(page).to have_selector('ul.breadcrumb a', count: 4)
@@ -69,8 +126,11 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I visit big commit page' do
- stub_const('Commit::DIFF_SAFE_FILES', 20)
- visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id)
+ # Create a temporary scope to ensure that the stub_const is removed after user
+ RSpec::Mocks.with_temporary_scope do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_lines: 1, max_files: 1 })
+ visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id)
+ end
end
step 'I see big commit warning' do
@@ -124,4 +184,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "build: pending"
expect(page).to have_content "1 build"
end
+
+ step 'I search "submodules" commits' do
+ fill_in 'commits-search', with: 'submodules'
+ end
+
+ step 'I should see only "submodules" commits' do
+ expect(page).to have_content "More submodules"
+ expect(page).not_to have_content "Change some files"
+ end
end
diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb
new file mode 100644
index 00000000000..94a5d4e2e4d
--- /dev/null
+++ b/features/steps/project/commits/revert.rb
@@ -0,0 +1,40 @@
+class Spinach::Features::RevertCommits < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+ include SharedDiffNote
+ include RepoHelpers
+
+ step 'I click on commit link' do
+ visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id)
+ end
+
+ step 'I click on the revert button' do
+ find("a[href='#modal-revert-commit']").click
+ end
+
+ step 'I revert the changes directly' do
+ page.within('#modal-revert-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Revert'
+ end
+ end
+
+ step 'I should see the revert commit notice' do
+ page.should have_content('The commit has been successfully reverted.')
+ end
+
+ step 'I should see a revert error' do
+ page.should have_content('Sorry, we cannot revert this commit automatically.')
+ end
+
+ step 'I revert the changes in a new merge request' do
+ page.within('#modal-revert-commit') do
+ click_button 'Revert'
+ end
+ end
+
+ step 'I should see the new merge request notice' do
+ page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+ end
+end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index b0230add34f..527f7853da9 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -30,4 +30,54 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
click_link current_user.name
end
end
+
+ step 'I should see "New merge request"' do
+ expect(page).to have_content(/new merge request/i)
+ end
+
+ step 'I goto the Merge Requests page' do
+ page.within '.page-sidebar-expanded' do
+ click_link "Merge Requests"
+ end
+ end
+
+ step 'I click link "New merge request"' do
+ expect(page).to have_content(/new merge request/i)
+ click_link "New Merge Request"
+ end
+
+ step 'I should see the new merge request page for my namespace' do
+ current_path.should have_content(/#{current_user.namespace.name}/i)
+ end
+
+ step 'I visit the forks page of the "Shop" project' do
+ @project = Project.where(name: 'Shop').last
+ visit namespace_project_forks_path(@project.namespace, @project)
+ end
+
+ step 'I should see my fork on the list' do
+ page.within('.projects-list-holder') do
+ project = @user.fork_of(@project)
+ expect(page).to have_content("#{project.namespace.human_name} / #{project.name}")
+ end
+ end
+
+ step 'I make forked repo invalid' do
+ project = @user.fork_of(@project)
+ project.path = 'test-crappy-path'
+ project.save!
+ end
+
+ step 'There is an existent fork of the "Shop" project' do
+ user = create(:user, name: 'Mike')
+ @forked_project = Projects::ForkService.new(@project, user).execute
+ end
+
+ step 'I should not see the other fork listed' do
+ expect(page).not_to have_content("#{@forked_project.namespace.human_name} / #{@forked_project.name}")
+ end
+
+ step 'I should see a private fork notice' do
+ expect(page).to have_content("1 private fork")
+ end
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index cbdce78dc0c..7e4425ff662 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -43,7 +43,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
expect(page).to have_css("h3.page-title", text: "New Merge Request")
- fill_in "merge_request_title", with: "Merge Request On Forked Project"
+ page.within 'form#new_merge_request' do
+ fill_in "merge_request_title", with: "Merge Request On Forked Project"
+ end
end
step 'I submit the merge request' do
diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb
index be4db770948..4994df589a7 100644
--- a/features/steps/project/hooks.rb
+++ b/features/steps/project/hooks.rb
@@ -25,14 +25,14 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
step 'I submit new hook' do
@url = FFaker::Internet.uri("http")
fill_in "hook_url", with: @url
- expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1)
+ expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
end
step 'I submit new hook with SSL verification enabled' do
@url = FFaker::Internet.uri("http")
fill_in "hook_url", with: @url
check "hook_enable_ssl_verification"
- expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1)
+ expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
end
step 'I should see newly created hook' do
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 2c2ed08655e..c5d45709b44 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -8,24 +8,32 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
visit namespace_project_issue_path(@project.namespace, @project, @issue)
end
+ step 'I click the thumbsup award Emoji' do
+ page.within '.awards' do
+ thumbsup = page.first('.award-control')
+ thumbsup.click
+ thumbsup.hover
+ end
+ end
+
step 'I click to emoji-picker' do
- page.within '.awards-controls' do
- page.find('.add-award').click
+ page.within '.awards' do
+ page.find('.js-add-award').click
end
end
step 'I click to emoji in the picker' do
page.within '.emoji-menu-content' do
- page.first('.emoji-icon').click
+ page.first('.js-emoji-btn').click
end
end
step 'I can remove it by clicking to icon' do
page.within '.awards' do
expect do
- page.find('.award.active').click
+ page.find('.js-emoji-btn.active').click
sleep 0.3
- end.to change{ page.all(".award").size }.from(3).to(2)
+ end.to change{ page.all(".award-control.js-emoji-btn").size }.from(3).to(2)
end
end
@@ -38,8 +46,25 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I have award added' do
page.within '.awards' do
- expect(page).to have_selector '.award'
- expect(page.find('.award.active .counter')).to have_content '1'
+ expect(page).to have_selector '.js-emoji-btn'
+ expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
+ end
+ end
+
+ step 'I have no awards added' do
+ page.within '.awards' do
+ expect(page).to have_selector '.award-control.js-emoji-btn'
+ expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
+
+ # Check tooltip data
+ page.all('.award-control.js-emoji-btn').each do |element|
+ expect(element['title']).to eq("")
+ end
+
+ page.all('.award-control .js-counter').each do |element|
+ expect(element).to have_content '0'
+ end
end
end
@@ -51,7 +76,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I leave comment with a single emoji' do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: ':smile:'
- click_button 'Add Comment'
+ click_button 'Comment'
end
end
@@ -66,4 +91,13 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
expect(page).to have_selector '[data-emoji="raised_hand"]'
end
end
+
+ step 'The emoji menu is visible' do
+ page.find(".emoji-menu.is-visible")
+ end
+
+ step 'The search field is focused' do
+ expect(page).to have_selector('#emoji_search')
+ expect(page.evaluate_script('document.activeElement.id')).to eq('emoji_search')
+ end
end
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
index 50bb32429b9..6d50501a722 100644
--- a/features/steps/project/issues/filter_labels.rb
+++ b/features/steps/project/issues/filter_labels.rb
@@ -29,7 +29,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end
step 'I click link "bug"' do
- select2('bug', from: "#label_name")
+ page.find('.js-label-select').click
+ sleep 0.5
+ execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
+ sleep 2
end
step 'I click link "feature"' do
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 8e8c9c57452..8c31fa890b2 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -27,7 +27,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click link "Closed"' do
- click_link "Closed"
+ find('.issues-state-filters a', text: "Closed").click
end
step 'I click button "Unsubscribe"' do
@@ -54,19 +54,24 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
expect(page).to have_content "Release 0.4"
end
+ step 'I should see issue "Tweet control"' do
+ expect(page).to have_content "Tweet control"
+ end
+
step 'I click link "New Issue"' do
click_link "New Issue"
end
step 'I click "author" dropdown' do
- first('#s2id_author_id').click
+ page.find('.js-author-search').click
+ sleep 1
end
step 'I see current user as the first user' do
- expect(page).to have_selector('.user-result', visible: true, count: 3)
- users = page.all('.user-name')
+ expect(page).to have_selector('.dropdown-content', visible: true)
+ users = page.all('.dropdown-menu-author .dropdown-content li a')
expect(users[0].text).to eq 'Any Author'
- expect(users[1].text).to eq current_user.name
+ expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}"
end
step 'I submit new issue "500 error on profile"' do
@@ -170,6 +175,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
author: project.users.first)
end
+ step 'project "Shop" have "Bugfix" open issue' do
+ create(:issue,
+ title: "Bugfix",
+ project: project,
+ author: project.users.first)
+ end
+
step 'project "Shop" have "Release 0.3" closed issue' do
create(:closed_issue,
title: "Release 0.3",
@@ -177,6 +189,56 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
author: project.users.first)
end
+ step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do
+ issue = Issue.find_by(title: 'Release 0.4')
+ create_list(:upvote_note, 2, project: project, noteable: issue)
+ create(:downvote_note, project: project, noteable: issue)
+ end
+
+ step 'issue "Tweet control" have 1 upvote and 2 downvotes' do
+ issue = Issue.find_by(title: 'Tweet control')
+ create(:upvote_note, project: project, noteable: issue)
+ create_list(:downvote_note, 2, project: project, noteable: issue)
+ end
+
+ step 'The list should be sorted by "Least popular"' do
+ page.within '.issues-list' do
+ page.within 'li.issue:nth-child(1)' do
+ expect(page).to have_content 'Tweet control'
+ expect(page).to have_content '1 2'
+ end
+
+ page.within 'li.issue:nth-child(2)' do
+ expect(page).to have_content 'Release 0.4'
+ expect(page).to have_content '2 1'
+ end
+
+ page.within 'li.issue:nth-child(3)' do
+ expect(page).to have_content 'Bugfix'
+ expect(page).to_not have_content '0 0'
+ end
+ end
+ end
+
+ step 'The list should be sorted by "Most popular"' do
+ page.within '.issues-list' do
+ page.within 'li.issue:nth-child(1)' do
+ expect(page).to have_content 'Release 0.4'
+ expect(page).to have_content '2 1'
+ end
+
+ page.within 'li.issue:nth-child(2)' do
+ expect(page).to have_content 'Tweet control'
+ expect(page).to have_content '1 2'
+ end
+
+ page.within 'li.issue:nth-child(3)' do
+ expect(page).to have_content 'Bugfix'
+ expect(page).to_not have_content '0 0'
+ end
+ end
+ end
+
step 'empty project "Empty Project"' do
create :empty_project, name: 'Empty Project', namespace: @user.namespace
end
@@ -206,7 +268,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'I leave a comment with code block' do
page.within(".js-main-target-form") do
fill_in "note[note]", with: "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```"
- click_button "Add Comment"
+ click_button "Comment"
sleep 0.05
end
end
@@ -293,7 +355,9 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
expect(page).to have_content('Yay!')
end
end
+
def filter_issue(text)
fill_in 'issue_search', with: text
end
+
end
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index e2eda511497..4faa0f4707c 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -59,7 +59,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end
step 'I should see 3 issues' do
- expect(page).to have_selector('#tab-issues li.issue-row', count: 4)
+ expect(page).to have_selector('#tab-issues li.issuable-row', count: 4)
end
step 'I click link to remove milestone' do
diff --git a/features/steps/project/issues/references.rb b/features/steps/project/issues/references.rb
new file mode 100644
index 00000000000..69e8b5cbde5
--- /dev/null
+++ b/features/steps/project/issues/references.rb
@@ -0,0 +1,7 @@
+class Spinach::Features::ProjectIssuesReferences < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedIssuable
+ include SharedNote
+ include SharedProject
+ include SharedUser
+end
diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb
new file mode 100644
index 00000000000..17944527e3a
--- /dev/null
+++ b/features/steps/project/labels.rb
@@ -0,0 +1,34 @@
+class Spinach::Features::Labels < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedIssuable
+ include SharedProject
+ include SharedNote
+ include SharedPaths
+ include SharedMarkdown
+
+ step 'And I visit project "Shop" labels page' do
+ visit namespace_project_labels_path(project.namespace, project)
+ end
+
+ step 'I should see that I am subscribed to the "bug" label' do
+ expect(subscribe_button).to have_content 'Unsubscribe'
+ end
+
+ step 'I should see that I am not subscribed to the "bug" label' do
+ expect(subscribe_button).to have_content 'Subscribe'
+ end
+
+ step 'I click button "Unsubscribe" for the "bug" label' do
+ subscribe_button.click
+ end
+
+ step 'I click button "Subscribe" for the "bug" label' do
+ subscribe_button.click
+ end
+
+ private
+
+ def subscribe_button
+ first('.subscribe-button span')
+ end
+end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index be993d11093..91fe19dd477 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -16,10 +16,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_link "Bug NS-04"
end
+ step 'I click link "Feature NS-05"' do
+ click_link "Feature NS-05"
+ end
+
step 'I click link "All"' do
click_link "All"
end
+ step 'I click link "Merged"' do
+ click_link "Merged"
+ end
+
step 'I click link "Closed"' do
click_link "Closed"
end
@@ -40,8 +48,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content "Bug NS-04"
end
+ step 'I should see merge request "Feature NS-05"' do
+ expect(page).to have_content "Feature NS-05"
+ end
+
step 'I should not see "master" branch' do
- expect(page).not_to have_content "master"
+ expect(find('.merge-request-info')).not_to have_content "master"
end
step 'I should see "other_branch" branch' do
@@ -60,7 +72,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).not_to have_content "Feature NS-03"
end
-
step 'I should not see "Bug NS-04" in merge requests' do
expect(page).not_to have_content "Bug NS-04"
end
@@ -121,6 +132,30 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
author: project.users.first)
end
+ step 'project "Shop" have "Feature NS-05" merged merge request' do
+ create(:merged_merge_request,
+ title: "Feature NS-05",
+ source_project: project,
+ target_project: project,
+ author: project.users.first)
+ end
+
+ step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do
+ create(:merge_request, :rebased,
+ title: "Bug NS-07",
+ source_project: project,
+ target_project: project,
+ author: project.users.first)
+ end
+
+ step 'project "Shop" have "Bug NS-08" open merge request with diverged branch' do
+ create(:merge_request, :diverged,
+ title: "Bug NS-08",
+ source_project: project,
+ target_project: project,
+ author: project.users.first)
+ end
+
step 'project "Shop" have "Feature NS-03" closed merge request' do
create(:closed_merge_request,
title: "Feature NS-03",
@@ -138,6 +173,56 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
author: project.users.first)
end
+ step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
+ merge_request = MergeRequest.find_by(title: 'Bug NS-04')
+ create_list(:upvote_note, 2, project: project, noteable: merge_request)
+ create(:downvote_note, project: project, noteable: merge_request)
+ end
+
+ step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
+ merge_request = MergeRequest.find_by(title: 'Bug NS-06')
+ create(:upvote_note, project: project, noteable: merge_request)
+ create_list(:downvote_note, 2, project: project, noteable: merge_request)
+ end
+
+ step 'The list should be sorted by "Least popular"' do
+ page.within '.mr-list' do
+ page.within 'li.merge-request:nth-child(1)' do
+ expect(page).to have_content 'Bug NS-06'
+ expect(page).to have_content '1 2'
+ end
+
+ page.within 'li.merge-request:nth-child(2)' do
+ expect(page).to have_content 'Bug NS-04'
+ expect(page).to have_content '2 1'
+ end
+
+ page.within 'li.merge-request:nth-child(3)' do
+ expect(page).to have_content 'Bug NS-05'
+ expect(page).to_not have_content '0 0'
+ end
+ end
+ end
+
+ step 'The list should be sorted by "Most popular"' do
+ page.within '.mr-list' do
+ page.within 'li.merge-request:nth-child(1)' do
+ expect(page).to have_content 'Bug NS-04'
+ expect(page).to have_content '2 1'
+ end
+
+ page.within 'li.merge-request:nth-child(2)' do
+ expect(page).to have_content 'Bug NS-06'
+ expect(page).to have_content '1 2'
+ end
+
+ page.within 'li.merge-request:nth-child(3)' do
+ expect(page).to have_content 'Bug NS-05'
+ expect(page).to_not have_content '0 0'
+ end
+ end
+ end
+
step 'I click on the Changes tab' do
page.within '.merge-request-tabs' do
click_link 'Changes'
@@ -181,6 +266,15 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
leave_comment "Line is wrong"
end
+ step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do
+ mr = MergeRequest.find_by(title: "Bug NS-05")
+ create(:note_on_merge_request_diff, project: project,
+ noteable_id: mr.id,
+ author: user_exists("John Doe"),
+ line_code: sample_commit.line_code,
+ note: 'Line is wrong')
+ end
+
step 'I leave a comment like "Line is wrong" on diff in commit' do
click_diff_line(sample_commit.line_code)
leave_comment "Line is wrong"
@@ -238,6 +332,22 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
end
+ 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 sample_commit.line_code_path
+ page.should have_content "Line is wrong"
+ end
+ end
+
+ step 'I should see a badge of "1" next to the discussion link' do
+ expect_discussion_badge_to_have_counter("1")
+ end
+
+ step 'I should see a badge of "0" next to the discussion link' do
+ expect_discussion_badge_to_have_counter("0")
+ end
+
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"
@@ -329,7 +439,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.within(".js-discussion-note-form") do
fill_in "note_note", with: "Line is correct"
- click_button "Add Comment"
+ click_button "Comment"
end
page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do
@@ -342,7 +452,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.within(".js-discussion-note-form") do
fill_in "note_note", with: "Line is wrong on here"
- click_button "Add Comment"
+ click_button "Comment"
end
end
@@ -415,6 +525,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
end
+ step 'I should see the diverged commits count' do
+ page.within ".mr-source-target" do
+ expect(page).to have_content /([0-9]+ commits behind)/
+ end
+ end
+
+ step 'I should not see the diverged commits count' do
+ page.within ".mr-source-target" do
+ expect(page).not_to have_content /([0-9]+ commit[s]? behind)/
+ end
+ end
+
def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end
@@ -426,7 +548,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
def leave_comment(message)
page.within(".js-discussion-note-form", visible: true) do
fill_in "note_note", with: message
- click_button "Add Comment"
+ click_button "Comment"
end
page.within(".notes_holder", visible: true) do
expect(page).to have_content message
@@ -444,4 +566,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
def have_visible_content (text)
have_css("*", text: text, visible: true)
end
+
+ def expect_discussion_badge_to_have_counter(value)
+ page.within(".notes-tab .badge") do
+ page.should have_content value
+ end
+ end
end
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
index 2685f5fd6b4..4fda0731e2f 100644
--- a/features/steps/project/merge_requests/acceptance.rb
+++ b/features/steps/project/merge_requests/acceptance.rb
@@ -29,7 +29,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'There is an open Merge Request' do
@user = create(:user)
@project = create(:project, :public)
- @project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER)
+ @project_member = create(:project_member, :developer, user: @user, project: @project)
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end
diff --git a/features/steps/project/merge_requests/references.rb b/features/steps/project/merge_requests/references.rb
new file mode 100644
index 00000000000..ab2ae6847a2
--- /dev/null
+++ b/features/steps/project/merge_requests/references.rb
@@ -0,0 +1,7 @@
+class Spinach::Features::ProjectMergeRequestsReferences < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedIssuable
+ include SharedNote
+ include SharedProject
+ include SharedUser
+end
diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb
new file mode 100644
index 00000000000..efbc4831ce1
--- /dev/null
+++ b/features/steps/project/merge_requests/revert.rb
@@ -0,0 +1,56 @@
+class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
+ include LoginHelpers
+ include GitlabRoutingHelper
+
+ step 'I click on the revert button' do
+ find("a[href='#modal-revert-commit']").click
+ end
+
+ step 'I revert the changes directly' do
+ page.within('#modal-revert-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Revert'
+ end
+ end
+
+ step 'I should see the revert merge request notice' do
+ page.should have_content('The merge request has been successfully reverted.')
+ end
+
+ step 'I should not see the revert button' do
+ expect(page).not_to have_selector(:xpath, "a[href='#modal-revert-commit']")
+ end
+
+ step 'I am on the Merge Request detail page' do
+ visit merge_request_path(@merge_request)
+ end
+
+ step 'I click on Accept Merge Request' do
+ click_button('Accept Merge Request')
+ end
+
+ step 'I am signed in as a developer of the project' do
+ login_as(@user)
+ end
+
+ step 'There is an open Merge Request' do
+ @user = create(:user)
+ @project = create(:project, :public)
+ @project_member = create(:project_member, :developer, user: @user, project: @project)
+ @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
+ end
+
+ step 'I should see a revert error' do
+ page.should have_content('Sorry, we cannot revert this merge request automatically.')
+ end
+
+ step 'I revert the changes in a new merge request' do
+ page.within('#modal-revert-commit') do
+ click_button 'Revert'
+ end
+ end
+
+ step 'I should see the new merge request notice' do
+ page.should have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+ end
+end
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 7a83d32a240..9b59b682676 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -41,17 +41,14 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
When 'I switch ref to "feature"' do
select 'feature', from: 'ref'
- sleep 2
end
When 'I switch ref to "v1.0.0"' do
select 'v1.0.0', from: 'ref'
- sleep 2
end
When 'click "Show only selected branch" checkbox' do
find('#filter_ref').click
- sleep 2
end
step 'page should have content not containing "v1.0.0"' do
@@ -60,7 +57,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
end
- step 'page should not have content not containing "v1.0.0"' do
+ step 'page should have "v1.0.0" in title' do
+ expect(page).to have_css 'title', text: 'Network · v1.0.0', visible: false
+ end
+
+ step 'page should only have content from "v1.0.0"' do
page.within '.network-graph' do
expect(page).not_to have_content 'Change some files'
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 37bf52b4a95..ef185861e00 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -144,4 +144,14 @@ class Spinach::Features::Project < Spinach::FeatureSteps
expect(page).to have_content 'Notification settings saved'
end
end
+
+ step 'I create bare repo' do
+ click_link 'Create empty bare repository'
+ end
+
+ step 'I should see command line instructions' do
+ page.within ".empty_wrapper" do
+ expect(page).to have_content("Command line instructions")
+ end
+ end
end
diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb
new file mode 100644
index 00000000000..8c1d09d6cc6
--- /dev/null
+++ b/features/steps/project/project_find_file.rb
@@ -0,0 +1,73 @@
+class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+ include SharedProjectTab
+
+ step 'I press "t"' do
+ find('body').native.send_key('t')
+ end
+
+ step 'I click Find File button' do
+ click_link 'Find File'
+ end
+
+ step 'I should see "find file" page' do
+ ensure_active_main_tab('Files')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ step 'I fill in Find by path with "git"' do
+ ensure_active_main_tab('Files')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ step 'I fill in file find with "git"' do
+ find_file "git"
+ end
+
+ step 'I fill in file find with "change"' do
+ find_file "change"
+ end
+
+ step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do
+ find_file "asdfghjklqwertyuizxcvbnm"
+ end
+
+ step 'I should see "VERSION" in files' do
+ expect(page).to have_content("VERSION")
+ end
+
+ step 'I should not see "VERSION" in files' do
+ expect(page).not_to have_content("VERSION")
+ end
+
+ step 'I should see "CHANGELOG" in files' do
+ expect(page).to have_content("CHANGELOG")
+ end
+
+ step 'I should not see "CHANGELOG" in files' do
+ expect(page).not_to have_content("CHANGELOG")
+ end
+
+ step 'I should see ".gitmodules" in files' do
+ expect(page).to have_content(".gitmodules")
+ end
+
+ step 'I should not see ".gitmodules" in files' do
+ expect(page).not_to have_content(".gitmodules")
+ end
+
+ step 'I should see ".gitignore" in files' do
+ expect(page).to have_content(".gitignore")
+ end
+
+ step 'I should not see ".gitignore" in files' do
+ expect(page).not_to have_content(".gitignore")
+ end
+
+
+ def find_file(text)
+ fill_in 'file_find', with: text
+ end
+end
diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb
new file mode 100644
index 00000000000..739a85e5fa4
--- /dev/null
+++ b/features/steps/project/project_group_links.rb
@@ -0,0 +1,50 @@
+class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+ include Select2Helper
+
+ step 'I should see project already shared with group "Ops"' do
+ page.within '.enabled-groups' do
+ expect(page).to have_content "Ops"
+ end
+ end
+
+ step 'I should see project is not shared with group "Market"' do
+ page.within '.enabled-groups' do
+ expect(page).not_to have_content "Market"
+ end
+ end
+
+ step 'I select group "Market" for share' do
+ group = Group.find_by(path: 'market')
+ select2(group.id, from: "#link_group_id")
+ select "Master", from: 'link_group_access'
+ click_button "Share"
+ end
+
+ step 'I should see project is shared with group "Market"' do
+ page.within '.enabled-groups' do
+ expect(page).to have_content "Market"
+ end
+ end
+
+ step 'project "Shop" is shared with group "Ops"' do
+ group = create(:group, name: 'Ops')
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = group.id
+ share_link.save!
+ end
+
+ step 'project "Shop" is not shared with group "Market"' do
+ create(:group, name: 'Market', path: 'market')
+ end
+
+ step 'I visit project group links page' do
+ visit namespace_project_group_links_path(project.namespace, project)
+ end
+
+ def project
+ @project ||= Project.find_by_name "Shop"
+ end
+end
diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb
new file mode 100644
index 00000000000..2508c09e36d
--- /dev/null
+++ b/features/steps/project/project_milestone.rb
@@ -0,0 +1,59 @@
+class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+
+ step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do
+ project = Project.find_by(name: "Shop")
+ milestone = project.milestones.find_by(title: 'v2.2')
+ issue = create(:issue, title: "Bugfix1", project: project, milestone: milestone)
+ issue.labels << project.labels.find_by(title: 'bug')
+ issue.labels << project.labels.find_by(title: 'feature')
+ end
+
+ step 'milestone has issue "Bugfix2" with labels: "bug", "enhancement"' do
+ project = Project.find_by(name: "Shop")
+ milestone = project.milestones.find_by(title: 'v2.2')
+ issue = create(:issue, title: "Bugfix2", project: project, milestone: milestone)
+ issue.labels << project.labels.find_by(title: 'bug')
+ issue.labels << project.labels.find_by(title: 'enhancement')
+ end
+
+ step 'project "Shop" has milestone "v2.2"' do
+ project = Project.find_by(name: "Shop")
+ milestone = create(:milestone,
+ title: "v2.2",
+ project: project,
+ description: "# Description header"
+ )
+ 3.times { create(:issue, project: project, milestone: milestone) }
+ end
+
+ step 'I should see the list of labels' do
+ expect(page).to have_selector('ul.manage-labels-list')
+ end
+
+ step 'I should see the labels "bug", "enhancement" and "feature"' do
+ page.within('#tab-issues') do
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'enhancement'
+ expect(page).to have_content 'feature'
+ end
+ end
+
+ step 'I should see the "bug" label listed only once' do
+ page.within('#tab-labels') do
+ expect(page).to have_content('bug', count: 1)
+ end
+ end
+
+ step 'I click link "v2.2"' do
+ click_link "v2.2"
+ end
+
+ step 'I click link "Labels"' do
+ page.within('.nav-links') do
+ page.find(:xpath, "//a[@href='#tab-labels']").click
+ end
+ end
+end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index 504654f90dd..786a0cad975 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -77,7 +77,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
step 'I leave a comment like "Good snippet!"' do
page.within('.js-main-target-form') do
fill_in "note_note", with: "Good snippet!"
- click_button "Add Comment"
+ click_button "Comment"
end
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index d08935aa101..243469b8e7d 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -52,7 +52,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see raw file content' do
- expect(source).to eq sample_blob.data
+ expect(source).to eq '' # Body is filled in by gitlab-workhorse
end
step 'I click button "Edit"' do
@@ -351,6 +351,19 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
+ # SVG files
+ step 'I upload a new SVG file' do
+ drop_in_dropzone test_svg_file
+ end
+
+ step 'I visit the SVG file' do
+ visit namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/logo_sample.svg')
+ end
+
+ step 'I can see the new rendered SVG image' do
+ expect(page).to have_css('.file-content img')
+ end
+
private
def set_new_content
@@ -410,4 +423,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
def test_image_file
File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
end
+
+ def test_svg_file
+ File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg')
+ end
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index 3a4f7a6e01c..2134dae168a 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -238,7 +238,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see new wiki page named test' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "test")
- expect(page).to have_content "Edit Page test"
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content "Test"
+ expect(page).to have_content "Edit Page"
+ end
end
When 'I go back to wiki page home' do
@@ -252,7 +256,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see Gitlab API document' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api")
- expect(page).to have_content "Edit Page api"
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content "Edit"
+ expect(page).to have_content "Api"
+ end
end
step 'I click on Rake tasks link' do
@@ -261,7 +269,11 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see Rake tasks directory' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks")
- expect(page).to have_content "Edit Page raketasks"
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content "Edit"
+ expect(page).to have_content "Rake"
+ end
end
step 'I go directory which contains README file' do
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index caad52def79..3fbcf770b62 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -123,4 +123,23 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
click_link('Remove user from team')
end
end
+
+ step 'I share project with group "OpenSource"' do
+ project = Project.find_by(name: 'Shop')
+ os_group = create(:group, name: 'OpenSource')
+ create(:project, group: os_group)
+ @os_user1 = create(:user)
+ @os_user2 = create(:user)
+ os_group.add_owner(@os_user1)
+ os_group.add_user(@os_user2, Gitlab::Access::DEVELOPER)
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER)
+ share_link.group_id = os_group.id
+ share_link.save!
+ end
+
+ step 'I should see "Opensource" group user listing' do
+ expect(page).to have_content("Shared with OpenSource group, members with Master role (2)")
+ expect(page).to have_content(@os_user1.name)
+ expect(page).to have_content(@os_user2.name)
+ end
end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 91d227fadbf..223b7277b51 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -120,7 +120,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I should see the new wiki page form' do
expect(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Edit Page image.jpg')
+ expect(page).to have_content('Edit Page')
end
step 'I create a New page with paths' do
@@ -132,16 +132,6 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(current_path).to include 'one/two/three'
end
- step 'I create a New page with an invalid name' do
- click_on 'New Page'
- fill_in 'Page slug', with: 'invalid name'
- click_on 'Create Page'
- end
-
- step 'I should see an error message' do
- expect(page).to have_content "The page slug is invalid"
- end
-
step 'I should see non-escaped link in the pages list' do
expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']")
end
@@ -169,11 +159,13 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I should see the page history' do
- expect(page).to have_content('History for')
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content('History')
+ end
end
step 'I search for Wiki content' do
- fill_in "Search in this project", with: "wiki_content"
+ fill_in "Search", with: "wiki_content"
click_button "Search"
end
diff --git a/features/steps/search.rb b/features/steps/search.rb
index 79273cbad9a..0ad837ebe1d 100644
--- a/features/steps/search.rb
+++ b/features/steps/search.rb
@@ -18,6 +18,11 @@ class Spinach::Features::Search < Spinach::FeatureSteps
click_button "Search"
end
+ step 'I search for "rspec" on project page' do
+ fill_in "search", with: "rspec"
+ click_button "Go"
+ end
+
step 'I search for "Wiki content"' do
fill_in "dashboard_search", with: "content"
click_button "Search"
@@ -95,7 +100,7 @@ class Spinach::Features::Search < Spinach::FeatureSteps
step 'I should see "test_wiki" link in the search results' do
page.within('.results') do
- find(:css, '.search-results').should have_link 'test_wiki.md'
+ expect(find(:css, '.search-results')).to have_link 'test_wiki'
end
end
@@ -103,4 +108,8 @@ class Spinach::Features::Search < Spinach::FeatureSteps
@wiki = ::ProjectWiki.new(project, current_user)
@wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit")
end
+
+ step 'project "Shop" is public' do
+ project.update_attributes(visibility_level: Project::PUBLIC)
+ end
end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index eb2ccd9d01e..0bee91d758d 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -6,7 +6,7 @@ module SharedActiveTab
end
def ensure_active_sub_tab(content)
- expect(find('div.content ul.center-top-menu li.active')).to have_content(content)
+ expect(find('div.content ul.nav-links li.active')).to have_content(content)
end
def ensure_active_sub_nav(content)
@@ -18,7 +18,7 @@ module SharedActiveTab
end
step 'no other sub tabs should be active' do
- expect(page).to have_selector('div.content ul.center-top-menu li.active', count: 1)
+ expect(page).to have_selector('div.content ul.nav-links li.active', count: 1)
end
step 'no other sub navs should be active' do
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
new file mode 100644
index 00000000000..c4c7672a432
--- /dev/null
+++ b/features/steps/shared/builds.rb
@@ -0,0 +1,78 @@
+module SharedBuilds
+ include Spinach::DSL
+
+ step 'project has CI enabled' do
+ @project.enable_ci
+ end
+
+ step 'project has coverage enabled' do
+ @project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/)
+ end
+
+ step 'project has a recent build' do
+ @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha)
+ @build = create(:ci_build_with_coverage, commit: @ci_commit)
+ end
+
+ step 'recent build is successful' do
+ @build.update_column(:status, 'success')
+ end
+
+ step 'recent build failed' do
+ @build.update_column(:status, 'failed')
+ end
+
+ step 'project has another build that is running' do
+ create(:ci_build, commit: @ci_commit, name: 'second build', status: 'running')
+ end
+
+ step 'I visit recent build details page' do
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ step 'I visit project builds page' do
+ visit namespace_project_builds_path(@project.namespace, @project)
+ end
+
+ step 'recent build has artifacts available' do
+ artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
+ archive = fixture_file_upload(artifacts, 'application/zip')
+ @build.update_attributes(artifacts_file: archive)
+ end
+
+ step 'recent build has artifacts metadata available' do
+ metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+ gzip = fixture_file_upload(metadata, 'application/x-gzip')
+ @build.update_attributes(artifacts_metadata: gzip)
+ end
+
+ step 'recent build has a build trace' do
+ @build.trace = 'build trace'
+ end
+
+ step 'download of build artifacts archive starts' do
+ expect(page.response_headers['Content-Type']).to eq 'application/zip'
+ expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
+ end
+
+ step 'I access artifacts download page' do
+ visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build)
+ end
+
+ step 'I see details of a build' do
+ expect(page).to have_content "Build ##{@build.id}"
+ end
+
+ step 'I see build trace' do
+ expect(page).to have_css '#build-trace'
+ end
+
+ step 'I see the build' do
+ page.within('.build') do
+ expect(page).to have_content "##{@build.id}"
+ expect(page).to have_content @build.sha[0..7]
+ expect(page).to have_content @build.ref
+ expect(page).to have_content @build.name
+ end
+ end
+end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index c6a0ae2ba38..906b66a4a63 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -23,7 +23,7 @@ module SharedDiffNote
page.within(diff_file_selector) do
click_diff_line(sample_commit.line_code)
- page.within("form[rel$='#{sample_commit.line_code}']") do
+ page.within("form[id$='#{sample_commit.line_code}']") do
fill_in "note[note]", with: "Typo, please fix"
find(".js-comment-button").trigger("click")
sleep 0.05
@@ -33,7 +33,7 @@ module SharedDiffNote
step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do
click_parallel_diff_line(sample_commit.line_code, 'old')
- page.within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do
+ page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do
fill_in "note[note]", with: "Old comment"
find(".js-comment-button").trigger("click")
end
@@ -41,7 +41,7 @@ module SharedDiffNote
step 'I leave a diff comment in a parallel view on the right side like "New comment"' do
click_parallel_diff_line(sample_commit.line_code, 'new')
- page.within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do
+ page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do
fill_in "note[note]", with: "New comment"
find(".js-comment-button").trigger("click")
end
@@ -51,7 +51,7 @@ module SharedDiffNote
page.within(diff_file_selector) do
click_diff_line(sample_commit.line_code)
- page.within("form[rel$='#{sample_commit.line_code}']") do
+ page.within("form[id$='#{sample_commit.line_code}']") do
fill_in "note[note]", with: "Should fix it :smile:"
find('.js-md-preview-button').click
end
@@ -62,7 +62,7 @@ module SharedDiffNote
page.within(diff_file_selector) do
click_diff_line(sample_commit.del_line_code)
- page.within("form[rel$='#{sample_commit.del_line_code}']") do
+ page.within("form[id$='#{sample_commit.del_line_code}']") do
fill_in "note[note]", with: "DRY this up"
find('.js-md-preview-button').click
end
@@ -91,16 +91,16 @@ module SharedDiffNote
page.within(diff_file_selector) do
click_diff_line(sample_commit.line_code)
- page.within("form[rel$='#{sample_commit.line_code}']") do
+ page.within("form[id$='#{sample_commit.line_code}']") do
fill_in 'note[note]', with: ':smile:'
- click_button('Add Comment')
+ click_button('Comment')
end
end
end
step 'I submit the diff comment' do
page.within(diff_file_selector) do
- click_button("Add Comment")
+ click_button("Comment")
end
end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index e6d1b8b8efc..b6d70a26c21 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -5,6 +5,99 @@ module SharedIssuable
find(:css, '.issuable-edit').click
end
+ step 'project "Community" has "Community issue" open issue' do
+ create_issuable_for_project(
+ project_name: 'Community',
+ title: 'Community issue'
+ )
+ end
+
+ step 'project "Community" has "Community fix" open merge request' do
+ create_issuable_for_project(
+ project_name: 'Community',
+ type: :merge_request,
+ title: 'Community fix'
+ )
+ end
+
+ step 'project "Enterprise" has "Enterprise issue" open issue' do
+ create_issuable_for_project(
+ project_name: 'Enterprise',
+ title: 'Enterprise issue'
+ )
+ end
+
+ step 'project "Enterprise" has "Enterprise fix" open merge request' do
+ create_issuable_for_project(
+ project_name: 'Enterprise',
+ type: :merge_request,
+ title: 'Enterprise fix'
+ )
+ end
+
+ step 'I leave a comment referencing issue "Community issue"' do
+ leave_reference_comment(
+ issuable: Issue.find_by(title: 'Community issue'),
+ from_project_name: 'Enterprise'
+ )
+ end
+
+ step 'I leave a comment referencing issue "Community fix"' do
+ leave_reference_comment(
+ issuable: MergeRequest.find_by(title: 'Community fix'),
+ from_project_name: 'Enterprise'
+ )
+ end
+
+ step 'I visit issue page "Enterprise issue"' do
+ issue = Issue.find_by(title: 'Enterprise issue')
+ visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
+ end
+
+ step 'I visit merge request page "Enterprise fix"' do
+ mr = MergeRequest.find_by(title: 'Enterprise fix')
+ visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
+ end
+
+ step 'I visit issue page "Community issue"' do
+ issue = Issue.find_by(title: 'Community issue')
+ visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
+ end
+
+ step 'I visit issue page "Community fix"' do
+ mr = MergeRequest.find_by(title: 'Community fix')
+ visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
+ end
+
+ step 'I should not see any related merge requests' do
+ page.within '.issue-details' do
+ expect(page).not_to have_content('.merge-requests')
+ end
+ end
+
+ step 'I should see the "Enterprise fix" related merge request' do
+ page.within '.merge-requests' do
+ expect(page).to have_content('1 Related Merge Request')
+ expect(page).to have_content('Enterprise fix')
+ end
+ end
+
+ step 'I should see a note linking to "Enterprise fix" merge request' do
+ visible_note(
+ issuable: MergeRequest.find_by(title: 'Enterprise fix'),
+ from_project_name: 'Community',
+ user_name: 'Mary Jane'
+ )
+ end
+
+ step 'I should see a note linking to "Enterprise issue" issue' do
+ visible_note(
+ issuable: Issue.find_by(title: 'Enterprise issue'),
+ from_project_name: 'Community',
+ user_name: 'Mary Jane'
+ )
+ end
+
step 'I click link "Edit" for the merge request' do
edit_issuable
end
@@ -12,4 +105,102 @@ module SharedIssuable
step 'I click link "Edit" for the issue' do
edit_issuable
end
+
+ step 'I sort the list by "Oldest updated"' do
+ find('button.dropdown-toggle.btn').click
+ page.within('ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link "Oldest updated"
+ end
+ end
+
+ step 'I sort the list by "Least popular"' do
+ find('button.dropdown-toggle.btn').click
+
+ page.within('ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link 'Least popular'
+ end
+ end
+
+ step 'I sort the list by "Most popular"' do
+ find('button.dropdown-toggle.btn').click
+
+ page.within('ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link 'Most popular'
+ end
+ end
+
+ step 'The list should be sorted by "Oldest updated"' do
+ page.within('div.dropdown.inline.prepend-left-10') do
+ expect(page.find('button.dropdown-toggle.btn')).to have_content('Oldest updated')
+ end
+ end
+
+ step 'I should see "1 of 1" in the sidebar' do
+ expect_sidebar_content('1 of 1')
+ end
+
+ step 'I should see "1 of 2" in the sidebar' do
+ expect_sidebar_content('1 of 2')
+ end
+
+ step 'I should see "2 of 2" in the sidebar' do
+ expect_sidebar_content('2 of 2')
+ end
+
+ step 'I should see "3 of 3" in the sidebar' do
+ expect_sidebar_content('3 of 3')
+ end
+
+ step 'I click link "Next" in the sidebar' do
+ page.within '.issuable-sidebar' do
+ click_link 'Next'
+ end
+ end
+
+ def create_issuable_for_project(project_name:, title:, type: :issue)
+ project = Project.find_by(name: project_name)
+
+ attrs = {
+ title: title,
+ author: project.users.first,
+ description: '# Description header'
+ }
+
+ case type
+ when :issue
+ attrs.merge!(project: project)
+ when :merge_request
+ attrs.merge!(
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'master'
+ )
+ end
+
+ create(type, attrs)
+ end
+
+ def leave_reference_comment(issuable:, from_project_name:)
+ project = Project.find_by(name: from_project_name)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "##{issuable.to_reference(project)}"
+ click_button 'Comment'
+ end
+ end
+
+ def visible_note(issuable:, from_project_name:, user_name:)
+ project = Project.find_by(name: from_project_name)
+
+ expect(page).to have_content(user_name)
+ expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
+ end
+
+ def expect_sidebar_content(content)
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content content
+ end
+ end
+
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index f6aabfefeff..fb0462d6e04 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -17,7 +17,7 @@ module SharedNote
step 'I leave a comment like "XML attached"' do
page.within(".js-main-target-form") do
fill_in "note[note]", with: "XML attached"
- click_button "Add Comment"
+ click_button "Comment"
end
end
@@ -30,7 +30,7 @@ module SharedNote
step 'I submit the comment' do
page.within(".js-main-target-form") do
- click_button "Add Comment"
+ click_button "Comment"
end
end
@@ -106,12 +106,16 @@ module SharedNote
end
end
+ step 'I should see no notes at all' do
+ expect(page).to_not have_css('.note')
+ end
+
# Markdown
step 'I leave a comment with a header containing "Comment with a header"' do
page.within(".js-main-target-form") do
fill_in "note[note]", with: "# Comment with a header"
- click_button "Add Comment"
+ click_button "Comment"
sleep 0.05
end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index b33bd332655..2bd8ea745e4 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -7,6 +7,10 @@ module SharedPaths
visit new_project_path
end
+ step 'I visit login page' do
+ visit new_user_session_path
+ end
+
# ----------------------------------------
# User
# ----------------------------------------
@@ -23,6 +27,10 @@ module SharedPaths
visit group_path(Group.find_by(name: "Owned"))
end
+ step 'I visit group "Owned" activity page' do
+ visit activity_group_path(Group.find_by(name: "Owned"))
+ end
+
step 'I visit group "Owned" issues page' do
visit issues_group_path(Group.find_by(name: "Owned"))
end
@@ -103,6 +111,10 @@ module SharedPaths
visit dashboard_groups_path
end
+ step 'I visit dashboard todos page' do
+ visit dashboard_todos_path
+ end
+
step 'I should be redirected to the dashboard groups page' do
expect(current_path).to eq dashboard_groups_path
end
@@ -183,6 +195,10 @@ module SharedPaths
visit admin_groups_path
end
+ step 'I visit admin appearance page' do
+ visit admin_appearances_path
+ end
+
step 'I visit admin teams page' do
visit admin_teams_path
end
@@ -191,6 +207,10 @@ module SharedPaths
visit admin_application_settings_path
end
+ step 'I visit spam logs page' do
+ visit admin_spam_logs_path
+ end
+
step 'I visit applications page' do
visit admin_applications_path
end
@@ -259,6 +279,10 @@ module SharedPaths
visit namespace_project_deploy_keys_path(@project.namespace, @project)
end
+ step 'I visit project find file page' do
+ visit namespace_project_find_file_path(@project.namespace, @project, root_ref)
+ end
+
# ----------------------------------------
# "Shop" Project
# ----------------------------------------
@@ -368,13 +392,19 @@ module SharedPaths
end
step 'I visit merge request page "Bug NS-04"' do
- mr = MergeRequest.find_by(title: "Bug NS-04")
- visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
+ visit merge_request_path("Bug NS-04")
end
step 'I visit merge request page "Bug NS-05"' do
- mr = MergeRequest.find_by(title: "Bug NS-05")
- visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
+ visit merge_request_path("Bug NS-05")
+ end
+
+ step 'I visit merge request page "Bug NS-07"' do
+ visit merge_request_path("Bug NS-07")
+ end
+
+ step 'I visit merge request page "Bug NS-08"' do
+ visit merge_request_path("Bug NS-08")
end
step 'I visit merge request page "Bug CO-01"' do
@@ -435,6 +465,10 @@ module SharedPaths
visit namespace_project_path(project.namespace, project)
end
+ step "I should not see command line instructions" do
+ expect(page).not_to have_css('.empty_wrapper')
+ end
+
# ----------------------------------------
# Public Projects
# ----------------------------------------
@@ -479,6 +513,11 @@ module SharedPaths
Project.find_by!(name: 'Shop')
end
+ def merge_request_path(title)
+ mr = MergeRequest.find_by(title: title)
+ namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
+ end
+
# ----------------------------------------
# Errors
# ----------------------------------------
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index da643bf3ba9..b13e82f276b 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -7,6 +7,11 @@ module SharedProject
@project.team << [@user, :master]
end
+ step "project exists in some group namespace" do
+ @group = create(:group, name: 'some group')
+ @project = create(:project, namespace: @group)
+ end
+
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
@@ -98,6 +103,18 @@ module SharedProject
end
# ----------------------------------------
+ # Project permissions
+ # ----------------------------------------
+
+ step 'I am member of a project with a guest role' do
+ @project.team << [@user, Gitlab::Access::GUEST]
+ end
+
+ step 'I am member of a project with a reporter role' do
+ @project.team << [@user, Gitlab::Access::REPORTER]
+ end
+
+ # ----------------------------------------
# Visibility of archived project
# ----------------------------------------
@@ -161,24 +178,33 @@ module SharedProject
end
step '"John Doe" owns private project "Enterprise"' do
- user = user_exists("John Doe", username: "john_doe")
- project = Project.find_by(name: "Enterprise")
- project ||= create(:empty_project, name: "Enterprise", namespace: user.namespace)
- project.team << [user, :master]
+ user_owns_project(
+ user_name: 'John Doe',
+ project_name: 'Enterprise'
+ )
+ end
+
+ step '"Mary Jane" owns private project "Enterprise"' do
+ user_owns_project(
+ user_name: 'Mary Jane',
+ project_name: 'Enterprise'
+ )
end
step '"John Doe" owns internal project "Internal"' do
- user = user_exists("John Doe", username: "john_doe")
- project = Project.find_by(name: "Internal")
- project ||= create :empty_project, :internal, name: 'Internal', namespace: user.namespace
- project.team << [user, :master]
+ user_owns_project(
+ user_name: 'John Doe',
+ project_name: 'Internal',
+ visibility: :internal
+ )
end
step '"John Doe" owns public project "Community"' do
- user = user_exists("John Doe", username: "john_doe")
- project = Project.find_by(name: "Community")
- project ||= create :empty_project, :public, name: 'Community', namespace: user.namespace
- project.team << [user, :master]
+ user_owns_project(
+ user_name: 'John Doe',
+ project_name: 'Community',
+ visibility: :public
+ )
end
step 'public empty project "Empty Public Project"' do
@@ -213,4 +239,23 @@ module SharedProject
expect(page).to have_content("skipped")
end
end
+
+ step 'The project is internal' do
+ @project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ step 'public access for builds is enabled' do
+ @project.update(public_builds: true)
+ end
+
+ step 'public access for builds is disabled' do
+ @project.update(public_builds: false)
+ end
+
+ def user_owns_project(user_name:, project_name:, visibility: :private)
+ user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
+ project = Project.find_by(name: project_name)
+ project ||= create(:empty_project, visibility, name: project_name, namespace: user.namespace)
+ project.team << [user, :master]
+ end
end
diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb
index f0721094ee3..9856c510aa0 100644
--- a/features/steps/shared/user.rb
+++ b/features/steps/shared/user.rb
@@ -26,4 +26,20 @@ module SharedUser
step 'I have no ssh keys' do
@user.keys.delete_all
end
+
+ step 'I click on "Personal projects" tab' do
+ page.within '.nav-links' do
+ click_link 'Personal projects'
+ end
+
+ expect(page).to have_css('.tab-content #projects.active')
+ end
+
+ step 'I click on "Contributed projects" tab' do
+ page.within '.nav-links' do
+ click_link 'Contributed projects'
+ end
+
+ expect(page).to have_css('.tab-content #contributed.active')
+ end
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 4156c7ec484..fe9e39cf509 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -6,14 +6,10 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout)
+ Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
end
-Spinach.hooks.on_tag("javascript") do
- Capybara.current_driver = Capybara.javascript_driver
-end
-
-Capybara.default_wait_time = timeout
+Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = false
unless ENV['CI'] || ENV['CI_SERVER']
@@ -22,3 +18,7 @@ unless ENV['CI'] || ENV['CI_SERVER']
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
end
+
+Spinach.hooks.before_run do
+ TestEnv.warm_asset_cache
+end
diff --git a/features/support/env.rb b/features/support/env.rb
index 62c80b9c948..357d164d87f 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -14,6 +14,7 @@ require 'sidekiq/testing/inline'
require_relative 'capybara'
require_relative 'db_cleaner'
+require_relative 'rerun'
%w(select2_helper test_env repo_helpers).each do |f|
require Rails.root.join('spec', 'support', f)
diff --git a/features/support/rerun.rb b/features/support/rerun.rb
new file mode 100644
index 00000000000..8b176c5be89
--- /dev/null
+++ b/features/support/rerun.rb
@@ -0,0 +1,14 @@
+# The spinach-rerun-reporter doesn't define the on_undefined_step
+# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb
+module Spinach
+ class Reporter
+ class Rerun
+ def on_undefined_step(step_data, failure, step_definitions = nil)
+ super step_data, failure, step_definitions
+
+ # save feature file and scenario line
+ @rerun << "#{current_feature.filename}:#{current_scenario.line}"
+ end
+ end
+ end
+end
diff --git a/features/user.feature b/features/user.feature
index 35eae842e77..e0cadba30a1 100644
--- a/features/user.feature
+++ b/features/user.feature
@@ -5,10 +5,12 @@ Feature: User
# Signed out
+ @javascript
Scenario: I visit user "John Doe" page while not signed in when he owns a public project
Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community"
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should not see project "Internal"
@@ -16,28 +18,34 @@ Feature: User
# Signed in as someone else
+ @javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he owns a public project
Given "John Doe" owns public project "Community"
And "John Doe" owns internal project "Internal"
And I sign in as a user
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should see project "Internal"
And I should see project "Community"
+ @javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a public project
Given "John Doe" owns internal project "Internal"
And I sign in as a user
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should see project "Internal"
And I should not see project "Community"
+ @javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a project I can see
Given I sign in as a user
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should not see project "Internal"
@@ -45,19 +53,23 @@ Feature: User
# Signed in as the user himself
+ @javascript
Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has a public project
Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community"
And I sign in as "John Doe"
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should see project "Enterprise"
And I should see project "Internal"
And I should see project "Community"
+ @javascript
Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has no public project
Given I sign in as "John Doe"
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should see project "Enterprise"
And I should not see project "Internal"
@@ -68,6 +80,7 @@ Feature: User
Given I sign in as a user
And "John Doe" has contributions
When I visit user "John Doe" page
+ And I click on "Contributed projects" tab
Then I should see user "John Doe" page
And I should see contributed projects
And I should see contributions calendar
diff --git a/fixtures/emojis/aliases.json b/fixtures/emojis/aliases.json
index 547ce7978b3..d3831d8045b 100644
--- a/fixtures/emojis/aliases.json
+++ b/fixtures/emojis/aliases.json
@@ -1,22 +1,35 @@
-{
+{
"northeast_pointing_airplane":"airplane_northeast",
"small_airplane":"airplane_small",
"up_pointing_small_airplane":"airplane_small_up",
"up_pointing_airplane":"airplane_up",
"left_anger_bubble":"anger_left",
"right_anger_bubble":"anger_right",
+ "keycap_asterisk":"asterisk",
+ "atom_symbol":"atom",
"ballot_box_with_ballot":"ballot_box",
"ballot_box_with_bold_check":"ballot_box_check",
"ballot_box_with_script_x":"ballot_box_x",
"ballot_script_x":"ballot_x",
+ "person_with_ball":"basketball_player",
+ "person_with_ball_tone1":"basketball_player_tone1",
+ "person_with_ball_tone2":"basketball_player_tone2",
+ "person_with_ball_tone3":"basketball_player_tone3",
+ "person_with_ball_tone4":"basketball_player_tone4",
+ "person_with_ball_tone5":"basketball_player_tone5",
"beach_with_umbrella":"beach",
+ "umbrella_on_ground":"beach_umbrella",
"bellhop_bell":"bellhop",
+ "biohazard_sign":"biohazard",
"bouquet_of_flowers":"bouquet2",
+ "archery":"bow_and_arrow",
"bullhorn_with_sound_waves":"bullhorn_waves",
"pocket calculator":"calculator",
"spiral_calendar_pad":"calendar_spiral",
"card_file_box":"card_box",
"tape_cartridge":"cartridge",
+ "bottle_with_popping_cork":"champagne",
+ "cheese_wedge":"cheese",
"city_sunrise":"city_sunset",
"mantlepiece_clock":"clock",
"clockwise_right_and_left_semicircle_arrows":"clockwise_arrows",
@@ -30,6 +43,8 @@
"couple_with_heart_mm":"couple_mm",
"couple_with_heart_ww":"couple_ww",
"lower_left_crayon":"crayon",
+ "cricket_bat_ball":"cricket",
+ "latin_cross":"cross",
"heavy_latin_cross":"cross_heavy",
"white_latin_cross":"cross_white",
"black_skull_and_crossbones":"crossbones",
@@ -60,10 +75,13 @@
"al":"flag_al",
"am":"flag_am",
"ao":"flag_ao",
+ "aq":"flag_aq",
"ar":"flag_ar",
+ "as":"flag_as",
"at":"flag_at",
"au":"flag_au",
"aw":"flag_aw",
+ "ax":"flag_ax",
"az":"flag_az",
"ba":"flag_ba",
"bb":"flag_bb",
@@ -74,37 +92,47 @@
"bh":"flag_bh",
"bi":"flag_bi",
"bj":"flag_bj",
+ "bl":"flag_bl",
"waving_black_flag":"flag_black",
"bm":"flag_bm",
"bn":"flag_bn",
"bo":"flag_bo",
+ "bq":"flag_bq",
"br":"flag_br",
"bs":"flag_bs",
"bt":"flag_bt",
+ "bv":"flag_bv",
"bw":"flag_bw",
"by":"flag_by",
"bz":"flag_bz",
"ca":"flag_ca",
+ "cc":"flag_cc",
"congo":"flag_cd",
"cf":"flag_cf",
"cg":"flag_cg",
"ch":"flag_ch",
"ci":"flag_ci",
+ "ck":"flag_ck",
"chile":"flag_cl",
"cm":"flag_cm",
"cn":"flag_cn",
"co":"flag_co",
+ "cp":"flag_cp",
"cr":"flag_cr",
"cu":"flag_cu",
"cv":"flag_cv",
+ "cw":"flag_cw",
+ "cx":"flag_cx",
"cy":"flag_cy",
"cz":"flag_cz",
"de":"flag_de",
+ "dg":"flag_dg",
"dj":"flag_dj",
"dk":"flag_dk",
"dm":"flag_dm",
"do":"flag_do",
"dz":"flag_dz",
+ "ea":"flag_ea",
"ec":"flag_ec",
"ee":"flag_ee",
"eg":"flag_eg",
@@ -112,6 +140,7 @@
"er":"flag_er",
"es":"flag_es",
"et":"flag_et",
+ "eu":"flag_eu",
"fi":"flag_fi",
"fj":"flag_fj",
"fk":"flag_fk",
@@ -122,26 +151,34 @@
"gb":"flag_gb",
"gd":"flag_gd",
"ge":"flag_ge",
+ "gf":"flag_gf",
+ "gg":"flag_gg",
"gh":"flag_gh",
"gi":"flag_gi",
"gl":"flag_gl",
"gm":"flag_gm",
"gn":"flag_gn",
+ "gp":"flag_gp",
"gq":"flag_gq",
"gr":"flag_gr",
+ "gs":"flag_gs",
"gt":"flag_gt",
"gu":"flag_gu",
"gw":"flag_gw",
"gy":"flag_gy",
"hk":"flag_hk",
+ "hm":"flag_hm",
"hn":"flag_hn",
"hr":"flag_hr",
"ht":"flag_ht",
"hu":"flag_hu",
+ "ic":"flag_ic",
"indonesia":"flag_id",
"ie":"flag_ie",
"il":"flag_il",
+ "im":"flag_im",
"in":"flag_in",
+ "io":"flag_io",
"iq":"flag_iq",
"ir":"flag_ir",
"is":"flag_is",
@@ -176,6 +213,7 @@
"mc":"flag_mc",
"md":"flag_md",
"me":"flag_me",
+ "mf":"flag_mf",
"mg":"flag_mg",
"mh":"flag_mh",
"mk":"flag_mk",
@@ -183,6 +221,8 @@
"mm":"flag_mm",
"mn":"flag_mn",
"mo":"flag_mo",
+ "mp":"flag_mp",
+ "mq":"flag_mq",
"mr":"flag_mr",
"ms":"flag_ms",
"mt":"flag_mt",
@@ -195,6 +235,7 @@
"na":"flag_na",
"nc":"flag_nc",
"ne":"flag_ne",
+ "nf":"flag_nf",
"nigeria":"flag_ng",
"ni":"flag_ni",
"nl":"flag_nl",
@@ -211,12 +252,15 @@
"ph":"flag_ph",
"pk":"flag_pk",
"pl":"flag_pl",
+ "pm":"flag_pm",
+ "pn":"flag_pn",
"pr":"flag_pr",
"ps":"flag_ps",
"pt":"flag_pt",
"pw":"flag_pw",
"py":"flag_py",
"qa":"flag_qa",
+ "re":"flag_re",
"ro":"flag_ro",
"rs":"flag_rs",
"ru":"flag_ru",
@@ -230,20 +274,27 @@
"sg":"flag_sg",
"sh":"flag_sh",
"si":"flag_si",
+ "sj":"flag_sj",
"sk":"flag_sk",
"sl":"flag_sl",
"sm":"flag_sm",
"sn":"flag_sn",
"so":"flag_so",
"sr":"flag_sr",
+ "ss":"flag_ss",
"st":"flag_st",
"sv":"flag_sv",
+ "sx":"flag_sx",
"sy":"flag_sy",
"sz":"flag_sz",
+ "ta":"flag_ta",
+ "tc":"flag_tc",
"td":"flag_td",
+ "tf":"flag_tf",
"tg":"flag_tg",
"th":"flag_th",
"tj":"flag_tj",
+ "tk":"flag_tk",
"tl":"flag_tl",
"turkmenistan":"flag_tm",
"tn":"flag_tn",
@@ -255,12 +306,14 @@
"tz":"flag_tz",
"ua":"flag_ua",
"ug":"flag_ug",
+ "um":"flag_um",
"us":"flag_us",
"uy":"flag_uy",
"uz":"flag_uz",
"va":"flag_va",
"vc":"flag_vc",
"ve":"flag_ve",
+ "vg":"flag_vg",
"vi":"flag_vi",
"vn":"flag_vn",
"vu":"flag_vu",
@@ -269,6 +322,7 @@
"ws":"flag_ws",
"xk":"flag_xk",
"ye":"flag_ye",
+ "yt":"flag_yt",
"za":"flag_za",
"zm":"flag_zm",
"zw":"flag_zw",
@@ -281,12 +335,24 @@
"frame_with_tiles":"frame_tiles",
"frame_with_an_x":"frame_x",
"anguished":"frowning",
+ "white_frowning_face":"frowning2",
+ "hammer_and_pick":"hammer_pick",
"raised_hand_with_fingers_splayed":"hand_splayed",
"reversed_raised_hand_with_fingers_splayed":"hand_splayed_reverse",
+ "raised_hand_with_fingers_splayed_tone1":"hand_splayed_tone1",
+ "raised_hand_with_fingers_splayed_tone2":"hand_splayed_tone2",
+ "raised_hand_with_fingers_splayed_tone3":"hand_splayed_tone3",
+ "raised_hand_with_fingers_splayed_tone4":"hand_splayed_tone4",
+ "raised_hand_with_fingers_splayed_tone5":"hand_splayed_tone5",
"reversed_victory_hand":"hand_victory",
+ "face_with_head_bandage":"head_bandage",
+ "heavy_heart_exclamation_mark_ornament":"heart_exclamation",
"heart_with_tip_on_the_left":"heart_tip",
+ "helmet_with_white_cross":"helmet_with_cross",
"house_buildings":"homes",
+ "hot_dog":"hotdog",
"derelict_house_building":"house_abandoned",
+ "hugging_face":"hugging",
"circled_information_source":"info",
"desert_island":"island",
"up_pointing_military_airplane":"jet_up",
@@ -300,16 +366,36 @@
"left_hand_telephone_receiver":"left_receiver",
"man_in_business_suit_levitating":"levitate",
"weight_lifter":"lifter",
+ "weight_lifter_tone1":"lifter_tone1",
+ "weight_lifter_tone2":"lifter_tone2",
+ "weight_lifter_tone3":"lifter_tone3",
+ "weight_lifter_tone4":"lifter_tone4",
+ "weight_lifter_tone5":"lifter_tone5",
"light_mark":"light_check_mark",
+ "lion":"lion_face",
"world_map":"map",
"sports_medal":"medal",
+ "sign_of_the_horns":"metal",
+ "sign_of_the_horns_tone1":"metal_tone1",
+ "sign_of_the_horns_tone2":"metal_tone2",
+ "sign_of_the_horns_tone3":"metal_tone3",
+ "sign_of_the_horns_tone4":"metal_tone4",
+ "sign_of_the_horns_tone5":"metal_tone5",
"studio_microphone":"microphone2",
"reversed_hand_with_middle_finger_extended":"middle_finger",
+ "reversed_hand_with_middle_finger_extended_tone1":"middle_finger_tone1",
+ "reversed_hand_with_middle_finger_extended_tone2":"middle_finger_tone2",
+ "reversed_hand_with_middle_finger_extended_tone3":"middle_finger_tone3",
+ "reversed_hand_with_middle_finger_extended_tone4":"middle_finger_tone4",
+ "reversed_hand_with_middle_finger_extended_tone5":"middle_finger_tone5",
+ "money_mouth_face":"money_mouth",
"lightning_mood_bubble":"mood_bubble_lightning",
"lightning_mood":"mood_lightning",
"racing_motorcycle":"motorcycle",
"snow_capped_mountain":"mountain_snow",
"one_button_mouse":"mouse_one",
+ "three_button_mouse":"mouse_three_button",
+ "nerd_face":"nerd",
"three_networked_computers":"network",
"rolled_up_newspaper":"newspaper2",
"note_page":"note",
@@ -319,27 +405,40 @@
"spiral_note_pad":"notepad_spiral",
"oil_drum":"oil",
"grandma":"older_woman",
+ "grandma_tone1":"older_woman_tone1",
+ "grandma_tone2":"older_woman_tone2",
+ "grandma_tone3":"older_woman_tone3",
+ "grandma_tone4":"older_woman_tone4",
+ "grandma_tone5":"older_woman_tone5",
"optical_disc_icon":"optical_disk",
"lower_left_paintbrush":"paintbrush",
"linked_paperclips":"paperclips",
"national_park":"park",
+ "double_vertical_bar":"pause_button",
+ "peace_symbol":"peace",
"lower_left_ballpoint_pen":"pen_ballpoint",
"lower_left_fountain_pen":"pen_fountain",
"memo":"pencil",
"lower_left_pencil":"pencil3",
"black_pennant":"pennant_black",
"white_pennant":"pennant_white",
+ "table_tennis":"ping_pong",
"no_piracy":"piracy",
+ "worship_symbol":"place_of_worship",
"shit":"poop",
"hankey":"poop",
"poo":"poop",
"prohibited_sign":"prohibited",
"film_projector":"projector",
"racing_car":"race_car",
+ "radioactive_sign":"radioactive",
"railroad_track":"railway_track",
"right_speaker_with_one_sound_wave":"right_speaker_one",
"right_speaker_with_three_sound_waves":"right_speaker_three",
+ "robot_face":"robot",
+ "face_with_rolling_eyes":"rolling_eyes",
"skeleton":"skull",
+ "skull_and_crossbones":"skull_crossbones",
"slightly_frowning_face":"slight_frown",
"slightly_smiling_face":"slight_smile",
"speaking_head_in_silhouette":"speaking_head",
@@ -348,20 +447,53 @@
"three_speech_bubbles":"speech_three",
"two_speech_bubbles":"speech_two",
"sleuth_or_spy":"spy",
+ "sleuth_or_spy_tone1":"spy_tone1",
+ "sleuth_or_spy_tone2":"spy_tone2",
+ "sleuth_or_spy_tone3":"spy_tone3",
+ "sleuth_or_spy_tone4":"spy_tone4",
+ "sleuth_or_spy_tone5":"spy_tone5",
"portable_stereo":"stereo",
"black_touchtone_telephone":"telephone_black",
"white_touchtone_telephone":"telephone_white",
+ "face_with_thermometer":"thermometer_face",
+ "thinking_face":"thinking",
"left_thought_bubble":"thought_left",
"right_thought_bubble":"thought_right",
"reversed_thumbs_down_sign":"thumbs_down_reverse",
"reversed_thumbs_up_sign":"thumbs_up_reverse",
"-1":"thumbsdown",
+ "-1_tone1":"thumbsdown_tone1",
+ "-1_tone2":"thumbsdown_tone2",
+ "-1_tone3":"thumbsdown_tone3",
+ "-1_tone4":"thumbsdown_tone4",
+ "-1_tone5":"thumbsdown_tone5",
"+1":"thumbsup",
+ "+1_tone1":"thumbsup_tone1",
+ "+1_tone2":"thumbsup_tone2",
+ "+1_tone3":"thumbsup_tone3",
+ "+1_tone4":"thumbsup_tone4",
+ "+1_tone5":"thumbsup_tone5",
+ "thunder_cloud_and_rain":"thunder_cloud_rain",
"admission_tickets":"tickets",
+ "timer_clock":"timer",
"hammer_and_wrench":"tools",
+ "next_track":"track_next",
+ "previous_track":"track_previous",
"diesel_locomotive":"train_diesel",
"triangle_with_rounded_corners":"triangle_round",
"turned_ok_hand_sign":"turned_ok_hand",
+ "unicorn_face":"unicorn",
+ "upside_down_face":"upside_down",
+ "funeral_urn":"urn",
"raised_hand_with_part_between_middle_and_ring_fingers":"vulcan",
- "left_writing_hand":"writing_hand"
-} \ No newline at end of file
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone1":"vulcan_tone1",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone2":"vulcan_tone2",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone3":"vulcan_tone3",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone4":"vulcan_tone4",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone5":"vulcan_tone5",
+ "white_sun_behind_cloud":"white_sun_cloud",
+ "white_sun_behind_cloud_with_rain":"white_sun_rain_cloud",
+ "white_sun_with_small_cloud":"white_sun_small_cloud",
+ "left_writing_hand":"writing_hand",
+ "zipper_mouth_face":"zipper_mouth"
+}
diff --git a/fixtures/emojis/generate_aliases.rb b/fixtures/emojis/generate_aliases.rb
new file mode 100755
index 00000000000..8838fb9a3af
--- /dev/null
+++ b/fixtures/emojis/generate_aliases.rb
@@ -0,0 +1,18 @@
+#!/usr/bin/env ruby
+
+require 'json'
+
+aliases = {}
+
+index_file = File.expand_path("./index.json")
+index = JSON.parse(File.read(index_file))
+
+index.each_pair do |key, data|
+ data['aliases'].each do |a|
+ a.tr!(':', '')
+
+ aliases[a] = key
+ end
+end
+
+puts JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: '')
diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json
index 60ef2399e14..7f204c1a8e0 100644
--- a/fixtures/emojis/index.json
+++ b/fixtures/emojis/index.json
@@ -7,7 +7,21 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["numbers", "perfect", "score", "100", "percent", "a", "plus", "perfect", "school", "quiz", "score", "test", "exam"],
+ "keywords": [
+ "numbers",
+ "perfect",
+ "score",
+ "100",
+ "percent",
+ "a",
+ "plus",
+ "perfect",
+ "school",
+ "quiz",
+ "score",
+ "test",
+ "exam"
+ ],
"moji": "💯"
},
"1234": {
@@ -18,7 +32,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "numbers"],
+ "keywords": [
+ "blue-square",
+ "numbers"
+ ],
"moji": "🔢"
},
"8ball": {
@@ -29,7 +46,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["pool", "billiards", "eight ball", "pool", "pocket ball", "cue"],
+ "keywords": [
+ "pool",
+ "billiards",
+ "eight ball",
+ "pool",
+ "pocket ball",
+ "cue"
+ ],
"moji": "🎱"
},
"a": {
@@ -40,7 +64,11 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "letter", "red-square"],
+ "keywords": [
+ "alphabet",
+ "letter",
+ "red-square"
+ ],
"moji": "🅰"
},
"ab": {
@@ -51,7 +79,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "red-square"],
+ "keywords": [
+ "alphabet",
+ "red-square"
+ ],
"moji": "🆎"
},
"abc": {
@@ -62,7 +93,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "blue-square"],
+ "keywords": [
+ "alphabet",
+ "blue-square"
+ ],
"moji": "🔤"
},
"abcd": {
@@ -73,7 +107,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "blue-square"],
+ "keywords": [
+ "alphabet",
+ "blue-square"
+ ],
"moji": "🔡"
},
"accept": {
@@ -84,7 +121,14 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["agree", "chinese", "good", "kanji", "ok", "yes"],
+ "keywords": [
+ "agree",
+ "chinese",
+ "good",
+ "kanji",
+ "ok",
+ "yes"
+ ],
"moji": "🉑"
},
"aerial_tramway": {
@@ -95,18 +139,42 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "aerial", "tram", "tramway", "cable", "transport"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "aerial",
+ "tram",
+ "tramway",
+ "cable",
+ "transport"
+ ],
"moji": "🚡"
},
"airplane": {
"unicode": "2708",
- "unicode_alternates": ["2708-FE0F"],
+ "unicode_alternates": [
+ "2708-FE0F"
+ ],
"name": "airplane",
"shortname": ":airplane:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flight", "transportation", "vehicle", "airplane", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"],
+ "keywords": [
+ "flight",
+ "transportation",
+ "vehicle",
+ "airplane",
+ "plane",
+ "airport",
+ "travel",
+ "airlines",
+ "fly",
+ "jet",
+ "jumbo",
+ "boeing",
+ "airbus"
+ ],
"moji": "✈"
},
"airplane_arriving": {
@@ -117,7 +185,20 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"]
+ "keywords": [
+ "flight",
+ "transportation",
+ "vehicle",
+ "plane",
+ "airport",
+ "travel",
+ "airlines",
+ "fly",
+ "jet",
+ "jumbo",
+ "boeing",
+ "airbus"
+ ]
},
"airplane_departure": {
"unicode": "1F6EB",
@@ -127,7 +208,21 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus", "leaving"]
+ "keywords": [
+ "flight",
+ "transportation",
+ "vehicle",
+ "plane",
+ "airport",
+ "travel",
+ "airlines",
+ "fly",
+ "jet",
+ "jumbo",
+ "boeing",
+ "airbus",
+ "leaving"
+ ]
},
"airplane_northeast": {
"unicode": "1F6EA",
@@ -135,9 +230,14 @@
"name": "northeast-pointing airplane",
"shortname": ":airplane_northeast:",
"category": "travel_places",
- "aliases": [":northeast_pointing_airplane:"],
+ "aliases": [
+ ":northeast_pointing_airplane:"
+ ],
"aliases_ascii": [],
- "keywords": ["plane", "travel"]
+ "keywords": [
+ "plane",
+ "travel"
+ ]
},
"airplane_small": {
"unicode": "1F6E9",
@@ -145,9 +245,24 @@
"name": "small airplane",
"shortname": ":airplane_small:",
"category": "travel_places",
- "aliases": [":small_airplane:"],
- "aliases_ascii": [],
- "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"]
+ "aliases": [
+ ":small_airplane:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "flight",
+ "transportation",
+ "vehicle",
+ "plane",
+ "airport",
+ "travel",
+ "airlines",
+ "fly",
+ "jet",
+ "jumbo",
+ "boeing",
+ "airbus"
+ ]
},
"airplane_small_up": {
"unicode": "1F6E8",
@@ -155,9 +270,14 @@
"name": "up-pointing small airplane",
"shortname": ":airplane_small_up:",
"category": "travel_places",
- "aliases": [":up_pointing_small_airplane:"],
+ "aliases": [
+ ":up_pointing_small_airplane:"
+ ],
"aliases_ascii": [],
- "keywords": ["plane", "travel"]
+ "keywords": [
+ "plane",
+ "travel"
+ ]
},
"airplane_up": {
"unicode": "1F6E7",
@@ -165,9 +285,14 @@
"name": "up-pointing airplane",
"shortname": ":airplane_up:",
"category": "travel_places",
- "aliases": [":up_pointing_airplane:"],
+ "aliases": [
+ ":up_pointing_airplane:"
+ ],
"aliases_ascii": [],
- "keywords": ["plane", "travel"]
+ "keywords": [
+ "plane",
+ "travel"
+ ]
},
"alarm_clock": {
"unicode": "23F0",
@@ -177,9 +302,26 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["time", "wake"],
+ "keywords": [
+ "time",
+ "wake"
+ ],
"moji": "⏰"
},
+ "alembic": {
+ "unicode": "2697",
+ "unicode_alternates": "",
+ "name": "alembic",
+ "shortname": ":alembic:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "chemistry",
+ "object",
+ "tool"
+ ]
+ },
"alien": {
"unicode": "1F47D",
"unicode_alternates": [],
@@ -188,7 +330,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["UFO", "paul", "alien", "ufo"],
+ "keywords": [
+ "UFO",
+ "paul",
+ "alien",
+ "ufo"
+ ],
"moji": "👽"
},
"ambulance": {
@@ -199,18 +346,50 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["911", "health", "ambulance", "emergency", "medical", "help", "assistance"],
+ "keywords": [
+ "911",
+ "health",
+ "ambulance",
+ "emergency",
+ "medical",
+ "help",
+ "assistance"
+ ],
"moji": "🚑"
},
+ "amphora": {
+ "unicode": "1F3FA",
+ "unicode_alternates": "",
+ "name": "amphora",
+ "shortname": ":amphora:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"anchor": {
"unicode": "2693",
- "unicode_alternates": ["2693-FE0F"],
+ "unicode_alternates": [
+ "2693-FE0F"
+ ],
"name": "anchor",
"shortname": ":anchor:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["ferry", "ship", "anchor", "ship", "boat", "ocean", "harbor", "marina", "shipyard", "sailor", "tattoo"],
+ "keywords": [
+ "ferry",
+ "ship",
+ "anchor",
+ "ship",
+ "boat",
+ "ocean",
+ "harbor",
+ "marina",
+ "shipyard",
+ "sailor",
+ "tattoo"
+ ],
"moji": "⚓"
},
"angel": {
@@ -221,9 +400,99 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["baby", "angel", "halo", "cupid", "wings", "halo", "heaven", "wings", "jesus"],
+ "keywords": [
+ "baby",
+ "angel",
+ "halo",
+ "cupid",
+ "wings",
+ "halo",
+ "heaven",
+ "wings",
+ "jesus"
+ ],
"moji": "👼"
},
+ "angel_tone1": {
+ "unicode": "1F47C-1F3FB",
+ "unicode_alternates": "",
+ "name": "baby angel tone 1",
+ "shortname": ":angel_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "halo",
+ "cupid",
+ "heaven",
+ "wings",
+ "jesus"
+ ]
+ },
+ "angel_tone2": {
+ "unicode": "1F47C-1F3FC",
+ "unicode_alternates": "",
+ "name": "baby angel tone 2",
+ "shortname": ":angel_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "halo",
+ "cupid",
+ "heaven",
+ "wings",
+ "jesus"
+ ]
+ },
+ "angel_tone3": {
+ "unicode": "1F47C-1F3FD",
+ "unicode_alternates": "",
+ "name": "baby angel tone 3",
+ "shortname": ":angel_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "halo",
+ "cupid",
+ "heaven",
+ "wings",
+ "jesus"
+ ]
+ },
+ "angel_tone4": {
+ "unicode": "1F47C-1F3FE",
+ "unicode_alternates": "",
+ "name": "baby angel tone 4",
+ "shortname": ":angel_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "halo",
+ "cupid",
+ "heaven",
+ "wings",
+ "jesus"
+ ]
+ },
+ "angel_tone5": {
+ "unicode": "1F47C-1F3FF",
+ "unicode_alternates": "",
+ "name": "baby angel tone 5",
+ "shortname": ":angel_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "halo",
+ "cupid",
+ "heaven",
+ "wings",
+ "jesus"
+ ]
+ },
"anger": {
"unicode": "1F4A2",
"unicode_alternates": [],
@@ -232,7 +501,11 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["anger", "angry", "mad"],
+ "keywords": [
+ "anger",
+ "angry",
+ "mad"
+ ],
"moji": "💢"
},
"anger_left": {
@@ -241,9 +514,20 @@
"name": "left anger bubble",
"shortname": ":anger_left:",
"category": "objects_symbols",
- "aliases": [":left_anger_bubble:"],
- "aliases_ascii": [],
- "keywords": ["speech", "balloon", "talk", "mood", "conversation", "communication", "comic", "angry"]
+ "aliases": [
+ ":left_anger_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "speech",
+ "balloon",
+ "talk",
+ "mood",
+ "conversation",
+ "communication",
+ "comic",
+ "angry"
+ ]
},
"anger_right": {
"unicode": "1F5EF",
@@ -251,9 +535,20 @@
"name": "right anger bubble",
"shortname": ":anger_right:",
"category": "objects_symbols",
- "aliases": [":right_anger_bubble:"],
- "aliases_ascii": [],
- "keywords": ["speech", "balloon", "talk", "mood", "conversation", "communication", "comic", "angry"]
+ "aliases": [
+ ":right_anger_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "speech",
+ "balloon",
+ "talk",
+ "mood",
+ "conversation",
+ "communication",
+ "comic",
+ "angry"
+ ]
},
"angry": {
"unicode": "1F620",
@@ -262,8 +557,22 @@
"shortname": ":angry:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [">:(", ">:-(", ":@"],
- "keywords": ["angry", "livid", "mad", "vexed", "irritated", "annoyed", "face", "frustrated", "mad"],
+ "aliases_ascii": [
+ ">:(",
+ ">:-(",
+ ":@"
+ ],
+ "keywords": [
+ "angry",
+ "livid",
+ "mad",
+ "vexed",
+ "irritated",
+ "annoyed",
+ "face",
+ "frustrated",
+ "mad"
+ ],
"moji": "😠"
},
"anguished": {
@@ -274,7 +583,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "nervous", "stunned", "pain", "anguish", "ouch", "misery", "distress", "grief"],
+ "keywords": [
+ "face",
+ "nervous",
+ "stunned",
+ "pain",
+ "anguish",
+ "ouch",
+ "misery",
+ "distress",
+ "grief"
+ ],
"moji": "😧"
},
"ant": {
@@ -285,7 +604,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "insect", "ant", "queen", "insect", "team"],
+ "keywords": [
+ "animal",
+ "insect",
+ "ant",
+ "queen",
+ "insect",
+ "team"
+ ],
"moji": "🐜"
},
"apple": {
@@ -296,40 +622,87 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fruit", "mac", "apple", "fruit", "electronics", "red", "doctor", "teacher", "school", "core"],
+ "keywords": [
+ "fruit",
+ "mac",
+ "apple",
+ "fruit",
+ "electronics",
+ "red",
+ "doctor",
+ "teacher",
+ "school",
+ "core"
+ ],
"moji": "🍎"
},
"aquarius": {
"unicode": "2652",
- "unicode_alternates": ["2652-FE0F"],
+ "unicode_alternates": [
+ "2652-FE0F"
+ ],
"name": "aquarius",
"shortname": ":aquarius:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["aquarius", "water", "bearer", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "aquarius",
+ "water",
+ "bearer",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "purple-square",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♒"
},
"aries": {
"unicode": "2648",
- "unicode_alternates": ["2648-FE0F"],
+ "unicode_alternates": [
+ "2648-FE0F"
+ ],
"name": "aries",
"shortname": ":aries:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["aries", "ram", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "aries",
+ "ram",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "purple-square",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♈"
},
"arrow_backward": {
"unicode": "25C0",
- "unicode_alternates": ["25C0-FE0F"],
+ "unicode_alternates": [
+ "25C0-FE0F"
+ ],
"name": "black left-pointing triangle",
"shortname": ":arrow_backward:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "◀"
},
"arrow_double_down": {
@@ -340,7 +713,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "⏬"
},
"arrow_double_up": {
@@ -351,18 +727,26 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "⏫"
},
"arrow_down": {
"unicode": "2B07",
- "unicode_alternates": ["2B07-FE0F"],
+ "unicode_alternates": [
+ "2B07-FE0F"
+ ],
"name": "downwards black arrow",
"shortname": ":arrow_down:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "⬇"
},
"arrow_down_small": {
@@ -373,117 +757,168 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "🔽"
},
"arrow_forward": {
"unicode": "25B6",
- "unicode_alternates": ["25B6-FE0F"],
+ "unicode_alternates": [
+ "25B6-FE0F"
+ ],
"name": "black right-pointing triangle",
"shortname": ":arrow_forward:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "▶"
},
"arrow_heading_down": {
"unicode": "2935",
- "unicode_alternates": ["2935-FE0F"],
+ "unicode_alternates": [
+ "2935-FE0F"
+ ],
"name": "arrow pointing rightwards then curving downwards",
"shortname": ":arrow_heading_down:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "⤵"
},
"arrow_heading_up": {
"unicode": "2934",
- "unicode_alternates": ["2934-FE0F"],
+ "unicode_alternates": [
+ "2934-FE0F"
+ ],
"name": "arrow pointing rightwards then curving upwards",
"shortname": ":arrow_heading_up:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "⤴"
},
"arrow_left": {
"unicode": "2B05",
- "unicode_alternates": ["2B05-FE0F"],
+ "unicode_alternates": [
+ "2B05-FE0F"
+ ],
"name": "leftwards black arrow",
"shortname": ":arrow_left:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square", "previous"],
+ "keywords": [
+ "arrow",
+ "blue-square",
+ "previous"
+ ],
"moji": "⬅"
},
"arrow_lower_left": {
"unicode": "2199",
- "unicode_alternates": ["2199-FE0F"],
+ "unicode_alternates": [
+ "2199-FE0F"
+ ],
"name": "south west arrow",
"shortname": ":arrow_lower_left:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "↙"
},
"arrow_lower_right": {
"unicode": "2198",
- "unicode_alternates": ["2198-FE0F"],
+ "unicode_alternates": [
+ "2198-FE0F"
+ ],
"name": "south east arrow",
"shortname": ":arrow_lower_right:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "blue-square"],
+ "keywords": [
+ "arrow",
+ "blue-square"
+ ],
"moji": "↘"
},
"arrow_right": {
"unicode": "27A1",
- "unicode_alternates": ["27A1-FE0F"],
+ "unicode_alternates": [
+ "27A1-FE0F"
+ ],
"name": "black rightwards arrow",
"shortname": ":arrow_right:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "next"],
+ "keywords": [
+ "blue-square",
+ "next"
+ ],
"moji": "➡"
},
"arrow_right_hook": {
"unicode": "21AA",
- "unicode_alternates": ["21AA-FE0F"],
+ "unicode_alternates": [
+ "21AA-FE0F"
+ ],
"name": "rightwards arrow with hook",
"shortname": ":arrow_right_hook:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "↪"
},
"arrow_up": {
"unicode": "2B06",
- "unicode_alternates": ["2B06-FE0F"],
+ "unicode_alternates": [
+ "2B06-FE0F"
+ ],
"name": "upwards black arrow",
"shortname": ":arrow_up:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "⬆"
},
"arrow_up_down": {
"unicode": "2195",
- "unicode_alternates": ["2195-FE0F"],
+ "unicode_alternates": [
+ "2195-FE0F"
+ ],
"name": "up down arrow",
"shortname": ":arrow_up_down:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "↕"
},
"arrow_up_small": {
@@ -494,29 +929,39 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "🔼"
},
"arrow_upper_left": {
"unicode": "2196",
- "unicode_alternates": ["2196-FE0F"],
+ "unicode_alternates": [
+ "2196-FE0F"
+ ],
"name": "north west arrow",
"shortname": ":arrow_upper_left:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "↖"
},
"arrow_upper_right": {
"unicode": "2197",
- "unicode_alternates": ["2197-FE0F"],
+ "unicode_alternates": [
+ "2197-FE0F"
+ ],
"name": "north east arrow",
"shortname": ":arrow_upper_right:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "↗"
},
"arrows_clockwise": {
@@ -527,7 +972,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sync"],
+ "keywords": [
+ "sync"
+ ],
"moji": "🔃"
},
"arrows_counterclockwise": {
@@ -538,7 +985,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "sync"],
+ "keywords": [
+ "blue-square",
+ "sync"
+ ],
"moji": "🔄"
},
"art": {
@@ -549,7 +999,20 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["design", "draw", "paint", "artist", "palette", "art", "colors", "paint", "draw", "brush", "pastels", "oils"],
+ "keywords": [
+ "design",
+ "draw",
+ "paint",
+ "artist",
+ "palette",
+ "art",
+ "colors",
+ "paint",
+ "draw",
+ "brush",
+ "pastels",
+ "oils"
+ ],
"moji": "🎨"
},
"articulated_lorry": {
@@ -560,7 +1023,16 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cars", "transportation", "vehicle", "truck", "delivery", "semi", "lorry", "articulated"],
+ "keywords": [
+ "cars",
+ "transportation",
+ "vehicle",
+ "truck",
+ "delivery",
+ "semi",
+ "lorry",
+ "articulated"
+ ],
"moji": "🚛"
},
"ascending_notes": {
@@ -571,7 +1043,28 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["score", "music", "sound", "tone"]
+ "keywords": [
+ "score",
+ "music",
+ "sound",
+ "tone"
+ ]
+ },
+ "asterisk": {
+ "unicode": "002A-20E3",
+ "unicode_alternates": "002a-fe0f-20e3",
+ "name": "keycap asterisk",
+ "shortname": ":asterisk:",
+ "category": "symbols",
+ "aliases": [
+ ":keycap_asterisk:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "*",
+ "star",
+ "symbol"
+ ]
},
"astonished": {
"unicode": "1F632",
@@ -581,7 +1074,13 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "xox", "shocked", "surprise", "astonished"],
+ "keywords": [
+ "face",
+ "xox",
+ "shocked",
+ "surprise",
+ "astonished"
+ ],
"moji": "😲"
},
"athletic_shoe": {
@@ -592,7 +1091,10 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shoes", "sports"],
+ "keywords": [
+ "shoes",
+ "sports"
+ ],
"moji": "👟"
},
"atm": {
@@ -603,9 +1105,38 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["atm", "cash", "withdrawal", "money", "deposit", "financial", "bank", "adam", "payday", "bank", "blue-square", "cash", "money", "payment"],
+ "keywords": [
+ "atm",
+ "cash",
+ "withdrawal",
+ "money",
+ "deposit",
+ "financial",
+ "bank",
+ "adam",
+ "payday",
+ "bank",
+ "blue-square",
+ "cash",
+ "money",
+ "payment"
+ ],
"moji": "🏧"
},
+ "atom": {
+ "unicode": "269B",
+ "unicode_alternates": "",
+ "name": "atom symbol",
+ "shortname": ":atom:",
+ "category": "symbols",
+ "aliases": [
+ ":atom_symbol:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "atheist"
+ ]
+ },
"b": {
"unicode": "1F171",
"unicode_alternates": [],
@@ -614,7 +1145,11 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "letter", "red-square"],
+ "keywords": [
+ "alphabet",
+ "letter",
+ "red-square"
+ ],
"moji": "🅱"
},
"baby": {
@@ -625,7 +1160,11 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["boy", "child", "infant"],
+ "keywords": [
+ "boy",
+ "child",
+ "infant"
+ ],
"moji": "👶"
},
"baby_bottle": {
@@ -636,7 +1175,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["container", "food", "baby", "bottle", "milk", "mother", "nipple", "newborn", "formula"],
+ "keywords": [
+ "container",
+ "food",
+ "baby",
+ "bottle",
+ "milk",
+ "mother",
+ "nipple",
+ "newborn",
+ "formula"
+ ],
"moji": "🍼"
},
"baby_chick": {
@@ -647,7 +1196,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "chicken", "chick", "baby", "bird", "chicken", "young", "woman", "cute"],
+ "keywords": [
+ "animal",
+ "chicken",
+ "chick",
+ "baby",
+ "bird",
+ "chicken",
+ "young",
+ "woman",
+ "cute"
+ ],
"moji": "🐤"
},
"baby_symbol": {
@@ -658,9 +1217,89 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["child", "orange-square", "baby", "crawl", "newborn", "human", "diaper", "small", "babe"],
+ "keywords": [
+ "child",
+ "orange-square",
+ "baby",
+ "crawl",
+ "newborn",
+ "human",
+ "diaper",
+ "small",
+ "babe"
+ ],
"moji": "🚼"
},
+ "baby_tone1": {
+ "unicode": "1F476-1F3FB",
+ "unicode_alternates": "",
+ "name": "baby tone 1",
+ "shortname": ":baby_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "child",
+ "infant",
+ "toddler"
+ ]
+ },
+ "baby_tone2": {
+ "unicode": "1F476-1F3FC",
+ "unicode_alternates": "",
+ "name": "baby tone 2",
+ "shortname": ":baby_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "child",
+ "infant",
+ "toddler"
+ ]
+ },
+ "baby_tone3": {
+ "unicode": "1F476-1F3FD",
+ "unicode_alternates": "",
+ "name": "baby tone 3",
+ "shortname": ":baby_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "child",
+ "infant",
+ "toddler"
+ ]
+ },
+ "baby_tone4": {
+ "unicode": "1F476-1F3FE",
+ "unicode_alternates": "",
+ "name": "baby tone 4",
+ "shortname": ":baby_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "child",
+ "infant",
+ "toddler"
+ ]
+ },
+ "baby_tone5": {
+ "unicode": "1F476-1F3FF",
+ "unicode_alternates": "",
+ "name": "baby tone 5",
+ "shortname": ":baby_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "child",
+ "infant",
+ "toddler"
+ ]
+ },
"back": {
"unicode": "1F519",
"unicode_alternates": [],
@@ -669,9 +1308,21 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow"],
+ "keywords": [
+ "arrow"
+ ],
"moji": "🔙"
},
+ "badminton": {
+ "unicode": "1F3F8",
+ "unicode_alternates": "",
+ "name": "badminton racquet",
+ "shortname": ":badminton:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"baggage_claim": {
"unicode": "1F6C4",
"unicode_alternates": [],
@@ -680,7 +1331,15 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["airport", "blue-square", "transport", "bag", "baggage", "luggage", "travel"],
+ "keywords": [
+ "airport",
+ "blue-square",
+ "transport",
+ "bag",
+ "baggage",
+ "luggage",
+ "travel"
+ ],
"moji": "🛄"
},
"balloon": {
@@ -691,7 +1350,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["celebration", "party", "balloon", "birthday", "celebration", "helium", "gas", "children", "float"],
+ "keywords": [
+ "celebration",
+ "party",
+ "balloon",
+ "birthday",
+ "celebration",
+ "helium",
+ "gas",
+ "children",
+ "float"
+ ],
"moji": "🎈"
},
"ballot_box": {
@@ -700,9 +1369,13 @@
"name": "ballot box with ballot",
"shortname": ":ballot_box:",
"category": "objects_symbols",
- "aliases": [":ballot_box_with_ballot:"],
+ "aliases": [
+ ":ballot_box_with_ballot:"
+ ],
"aliases_ascii": [],
- "keywords": ["vote"]
+ "keywords": [
+ "vote"
+ ]
},
"ballot_box_check": {
"unicode": "1F5F9",
@@ -710,19 +1383,29 @@
"name": "ballot box with bold check",
"shortname": ":ballot_box_check:",
"category": "objects_symbols",
- "aliases": [":ballot_box_with_bold_check:"],
+ "aliases": [
+ ":ballot_box_with_bold_check:"
+ ],
"aliases_ascii": [],
- "keywords": ["mark", "vote"]
+ "keywords": [
+ "mark",
+ "vote"
+ ]
},
"ballot_box_with_check": {
"unicode": "2611",
- "unicode_alternates": ["2611-FE0F"],
+ "unicode_alternates": [
+ "2611-FE0F"
+ ],
"name": "ballot box with check",
"shortname": ":ballot_box_with_check:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["agree", "ok"],
+ "keywords": [
+ "agree",
+ "ok"
+ ],
"moji": "☑"
},
"ballot_box_x": {
@@ -731,9 +1414,14 @@
"name": "ballot box with script x",
"shortname": ":ballot_box_x:",
"category": "objects_symbols",
- "aliases": [":ballot_box_with_script_x:"],
+ "aliases": [
+ ":ballot_box_with_script_x:"
+ ],
"aliases_ascii": [],
- "keywords": ["mark", "vote"]
+ "keywords": [
+ "mark",
+ "vote"
+ ]
},
"ballot_x": {
"unicode": "1F5F4",
@@ -741,9 +1429,14 @@
"name": "ballot script x",
"shortname": ":ballot_x:",
"category": "objects_symbols",
- "aliases": [":ballot_script_x:"],
+ "aliases": [
+ ":ballot_script_x:"
+ ],
"aliases_ascii": [],
- "keywords": ["mark", "vote"]
+ "keywords": [
+ "mark",
+ "vote"
+ ]
},
"bamboo": {
"unicode": "1F38D",
@@ -753,7 +1446,25 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "plant", "vegetable", "pine", "bamboo", "decoration", "new", "years", "spirits", "harvest", "prosperity", "longevity", "fortune", "luck", "welcome", "farming", "agriculture"],
+ "keywords": [
+ "nature",
+ "plant",
+ "vegetable",
+ "pine",
+ "bamboo",
+ "decoration",
+ "new",
+ "years",
+ "spirits",
+ "harvest",
+ "prosperity",
+ "longevity",
+ "fortune",
+ "luck",
+ "welcome",
+ "farming",
+ "agriculture"
+ ],
"moji": "🎍"
},
"banana": {
@@ -764,18 +1475,29 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "banana", "peel", "bunch"],
+ "keywords": [
+ "food",
+ "fruit",
+ "banana",
+ "peel",
+ "bunch"
+ ],
"moji": "🍌"
},
"bangbang": {
"unicode": "203C",
- "unicode_alternates": ["203C-FE0F"],
+ "unicode_alternates": [
+ "203C-FE0F"
+ ],
"name": "double exclamation mark",
"shortname": ":bangbang:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["exclamation", "surprise"],
+ "keywords": [
+ "exclamation",
+ "surprise"
+ ],
"moji": "‼"
},
"bank": {
@@ -786,7 +1508,9 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building"],
+ "keywords": [
+ "building"
+ ],
"moji": "🏦"
},
"bar_chart": {
@@ -797,7 +1521,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["graph", "presentation", "stats"],
+ "keywords": [
+ "graph",
+ "presentation",
+ "stats"
+ ],
"moji": "📊"
},
"barber": {
@@ -808,18 +1536,28 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["hair", "salon", "style"],
+ "keywords": [
+ "hair",
+ "salon",
+ "style"
+ ],
"moji": "💈"
},
"baseball": {
"unicode": "26BE",
- "unicode_alternates": ["26BE-FE0F"],
+ "unicode_alternates": [
+ "26BE-FE0F"
+ ],
"name": "baseball",
"shortname": ":baseball:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["MLB", "balls", "sports"],
+ "keywords": [
+ "MLB",
+ "balls",
+ "sports"
+ ],
"moji": "⚾"
},
"basketball": {
@@ -830,9 +1568,95 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["NBA", "balls", "sports", "basketball", "bball", "dribble", "hoop", "net", "swish", "rip city"],
+ "keywords": [
+ "NBA",
+ "balls",
+ "sports",
+ "basketball",
+ "bball",
+ "dribble",
+ "hoop",
+ "net",
+ "swish",
+ "rip city"
+ ],
"moji": "🏀"
},
+ "basketball_player": {
+ "unicode": "26F9",
+ "unicode_alternates": "",
+ "name": "person with ball",
+ "shortname": ":basketball_player:",
+ "category": "activity",
+ "aliases": [
+ ":person_with_ball:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "travel"
+ ]
+ },
+ "basketball_player_tone1": {
+ "unicode": "26F9-1F3FB",
+ "unicode_alternates": "",
+ "name": "person with ball tone 1",
+ "shortname": ":basketball_player_tone1:",
+ "category": "activity",
+ "aliases": [
+ ":person_with_ball_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "basketball_player_tone2": {
+ "unicode": "26F9-1F3FC",
+ "unicode_alternates": "",
+ "name": "person with ball tone 2",
+ "shortname": ":basketball_player_tone2:",
+ "category": "activity",
+ "aliases": [
+ ":person_with_ball_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "basketball_player_tone3": {
+ "unicode": "26F9-1F3FD",
+ "unicode_alternates": "",
+ "name": "person with ball tone 3",
+ "shortname": ":basketball_player_tone3:",
+ "category": "activity",
+ "aliases": [
+ ":person_with_ball_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "basketball_player_tone4": {
+ "unicode": "26F9-1F3FE",
+ "unicode_alternates": "",
+ "name": "person with ball tone 4",
+ "shortname": ":basketball_player_tone4:",
+ "category": "activity",
+ "aliases": [
+ ":person_with_ball_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "basketball_player_tone5": {
+ "unicode": "26F9-1F3FF",
+ "unicode_alternates": "",
+ "name": "person with ball tone 5",
+ "shortname": ":basketball_player_tone5:",
+ "category": "activity",
+ "aliases": [
+ ":person_with_ball_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"bath": {
"unicode": "1F6C0",
"unicode_alternates": [],
@@ -841,9 +1665,140 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clean", "shower", "bath", "tub", "basin", "wash", "bubble", "soak", "bathroom", "soap", "water", "clean", "shampoo", "lather", "water"],
+ "keywords": [
+ "clean",
+ "shower",
+ "bath",
+ "tub",
+ "basin",
+ "wash",
+ "bubble",
+ "soak",
+ "bathroom",
+ "soap",
+ "water",
+ "clean",
+ "shampoo",
+ "lather",
+ "water"
+ ],
"moji": "🛀"
},
+ "bath_tone1": {
+ "unicode": "1F6C0-1F3FB",
+ "unicode_alternates": "",
+ "name": "bath tone 1",
+ "shortname": ":bath_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "shower",
+ "tub",
+ "basin",
+ "wash",
+ "bubble",
+ "soak",
+ "bathroom",
+ "soap",
+ "water",
+ "clean",
+ "shampoo",
+ "lather"
+ ]
+ },
+ "bath_tone2": {
+ "unicode": "1F6C0-1F3FC",
+ "unicode_alternates": "",
+ "name": "bath tone 2",
+ "shortname": ":bath_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "shower",
+ "tub",
+ "basin",
+ "wash",
+ "bubble",
+ "soak",
+ "bathroom",
+ "soap",
+ "water",
+ "clean",
+ "shampoo",
+ "lather"
+ ]
+ },
+ "bath_tone3": {
+ "unicode": "1F6C0-1F3FD",
+ "unicode_alternates": "",
+ "name": "bath tone 3",
+ "shortname": ":bath_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "shower",
+ "tub",
+ "basin",
+ "wash",
+ "bubble",
+ "soak",
+ "bathroom",
+ "soap",
+ "water",
+ "clean",
+ "shampoo",
+ "lather"
+ ]
+ },
+ "bath_tone4": {
+ "unicode": "1F6C0-1F3FE",
+ "unicode_alternates": "",
+ "name": "bath tone 4",
+ "shortname": ":bath_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "shower",
+ "tub",
+ "basin",
+ "wash",
+ "bubble",
+ "soak",
+ "bathroom",
+ "soap",
+ "water",
+ "clean",
+ "shampoo",
+ "lather"
+ ]
+ },
+ "bath_tone5": {
+ "unicode": "1F6C0-1F3FF",
+ "unicode_alternates": "",
+ "name": "bath tone 5",
+ "shortname": ":bath_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "shower",
+ "tub",
+ "basin",
+ "wash",
+ "bubble",
+ "soak",
+ "bathroom",
+ "soap",
+ "water",
+ "clean",
+ "shampoo",
+ "lather"
+ ]
+ },
"bathtub": {
"unicode": "1F6C1",
"unicode_alternates": [],
@@ -852,7 +1807,23 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clean", "shower", "bath", "tub", "basin", "wash", "bubble", "soak", "bathroom", "soap", "water", "clean", "shampoo", "lather", "water"],
+ "keywords": [
+ "clean",
+ "shower",
+ "bath",
+ "tub",
+ "basin",
+ "wash",
+ "bubble",
+ "soak",
+ "bathroom",
+ "soap",
+ "water",
+ "clean",
+ "shampoo",
+ "lather",
+ "water"
+ ],
"moji": "🛁"
},
"battery": {
@@ -863,7 +1834,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["energy", "power", "sustain"],
+ "keywords": [
+ "energy",
+ "power",
+ "sustain"
+ ],
"moji": "🔋"
},
"beach": {
@@ -872,9 +1847,38 @@
"name": "beach with umbrella",
"shortname": ":beach:",
"category": "travel_places",
- "aliases": [":beach_with_umbrella:"],
- "aliases_ascii": [],
- "keywords": ["sand", "sun", "surf", "vacation", "relaxation", "tanning", "tan", "swimming"]
+ "aliases": [
+ ":beach_with_umbrella:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "sand",
+ "sun",
+ "surf",
+ "vacation",
+ "relaxation",
+ "tanning",
+ "tan",
+ "swimming"
+ ]
+ },
+ "beach_umbrella": {
+ "unicode": "26F1",
+ "unicode_alternates": "",
+ "name": "umbrella on ground",
+ "shortname": ":beach_umbrella:",
+ "category": "objects",
+ "aliases": [
+ ":umbrella_on_ground:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "nature",
+ "rain",
+ "sun",
+ "travel",
+ "weather"
+ ]
},
"bear": {
"unicode": "1F43B",
@@ -884,7 +1888,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐻"
},
"bed": {
@@ -895,7 +1902,15 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sleep", "sex", "queen", "full", "twin", "king", "mattress"]
+ "keywords": [
+ "sleep",
+ "sex",
+ "queen",
+ "full",
+ "twin",
+ "king",
+ "mattress"
+ ]
},
"bee": {
"unicode": "1F41D",
@@ -905,7 +1920,20 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "insect", "bee", "queen", "buzz", "flower", "pollen", "sting", "honey", "hive", "bumble", "pollination"],
+ "keywords": [
+ "animal",
+ "insect",
+ "bee",
+ "queen",
+ "buzz",
+ "flower",
+ "pollen",
+ "sting",
+ "honey",
+ "hive",
+ "bumble",
+ "pollination"
+ ],
"moji": "🐝"
},
"beer": {
@@ -916,7 +1944,26 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["beverage", "drink", "drunk", "party", "pub", "relax", "beer", "hops", "mug", "barley", "malt", "yeast", "portland", "oregon", "brewery", "micro", "pint", "boot"],
+ "keywords": [
+ "beverage",
+ "drink",
+ "drunk",
+ "party",
+ "pub",
+ "relax",
+ "beer",
+ "hops",
+ "mug",
+ "barley",
+ "malt",
+ "yeast",
+ "portland",
+ "oregon",
+ "brewery",
+ "micro",
+ "pint",
+ "boot"
+ ],
"moji": "🍺"
},
"beers": {
@@ -927,7 +1974,25 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["beverage", "drink", "drunk", "party", "pub", "relax", "beer", "beers", "cheers", "mug", "toast", "celebrate", "pub", "bar", "jolly", "hops", "clink"],
+ "keywords": [
+ "beverage",
+ "drink",
+ "drunk",
+ "party",
+ "pub",
+ "relax",
+ "beer",
+ "beers",
+ "cheers",
+ "mug",
+ "toast",
+ "celebrate",
+ "pub",
+ "bar",
+ "jolly",
+ "hops",
+ "clink"
+ ],
"moji": "🍻"
},
"beetle": {
@@ -938,7 +2003,19 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["insect", "nature", "lady", "bug", "ladybug", "ladybird", "beetle", "cow", "lady cow", "insect", "endearment"],
+ "keywords": [
+ "insect",
+ "nature",
+ "lady",
+ "bug",
+ "ladybug",
+ "ladybird",
+ "beetle",
+ "cow",
+ "lady cow",
+ "insect",
+ "endearment"
+ ],
"moji": "🐞"
},
"beginner": {
@@ -949,7 +2026,10 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["badge", "shield"],
+ "keywords": [
+ "badge",
+ "shield"
+ ],
"moji": "🔰"
},
"bell": {
@@ -960,7 +2040,13 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chime", "christmas", "notification", "sound", "xmas"],
+ "keywords": [
+ "chime",
+ "christmas",
+ "notification",
+ "sound",
+ "xmas"
+ ],
"moji": "🔔"
},
"bellhop": {
@@ -969,9 +2055,15 @@
"name": "bellhop bell",
"shortname": ":bellhop:",
"category": "travel_places",
- "aliases": [":bellhop_bell:"],
+ "aliases": [
+ ":bellhop_bell:"
+ ],
"aliases_ascii": [],
- "keywords": ["hotel", "porter", "ding"]
+ "keywords": [
+ "hotel",
+ "porter",
+ "ding"
+ ]
},
"bento": {
"unicode": "1F371",
@@ -981,7 +2073,19 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["box", "food", "japanese", "bento", "japanese", "rice", "meal", "box", "obento", "convenient", "lunchbox"],
+ "keywords": [
+ "box",
+ "food",
+ "japanese",
+ "bento",
+ "japanese",
+ "rice",
+ "meal",
+ "box",
+ "obento",
+ "convenient",
+ "lunchbox"
+ ],
"moji": "🍱"
},
"bicyclist": {
@@ -992,9 +2096,115 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bike", "exercise", "hipster", "sports", "bicyclist", "road", "bike", "pedal", "bicycle", "transportation"],
+ "keywords": [
+ "bike",
+ "exercise",
+ "hipster",
+ "sports",
+ "bicyclist",
+ "road",
+ "bike",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ],
"moji": "🚴"
},
+ "bicyclist_tone1": {
+ "unicode": "1F6B4-1F3FB",
+ "unicode_alternates": "",
+ "name": "bicyclist tone 1",
+ "shortname": ":bicyclist_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "bike",
+ "exercise",
+ "hipster",
+ "sport",
+ "road",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
+ "bicyclist_tone2": {
+ "unicode": "1F6B4-1F3FC",
+ "unicode_alternates": "",
+ "name": "bicyclist tone 2",
+ "shortname": ":bicyclist_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "bike",
+ "exercise",
+ "hipster",
+ "sport",
+ "road",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
+ "bicyclist_tone3": {
+ "unicode": "1F6B4-1F3FD",
+ "unicode_alternates": "",
+ "name": "bicyclist tone 3",
+ "shortname": ":bicyclist_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "bike",
+ "exercise",
+ "hipster",
+ "sport",
+ "road",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
+ "bicyclist_tone4": {
+ "unicode": "1F6B4-1F3FE",
+ "unicode_alternates": "",
+ "name": "bicyclist tone 4",
+ "shortname": ":bicyclist_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "bike",
+ "exercise",
+ "hipster",
+ "sport",
+ "road",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
+ "bicyclist_tone5": {
+ "unicode": "1F6B4-1F3FF",
+ "unicode_alternates": "",
+ "name": "bicyclist tone 5",
+ "shortname": ":bicyclist_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "bike",
+ "exercise",
+ "hipster",
+ "sport",
+ "road",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
"bike": {
"unicode": "1F6B2",
"unicode_alternates": [],
@@ -1003,7 +2213,16 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bicycle", "exercise", "hipster", "sports", "bike", "pedal", "bicycle", "transportation"],
+ "keywords": [
+ "bicycle",
+ "exercise",
+ "hipster",
+ "sports",
+ "bike",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ],
"moji": "🚲"
},
"bikini": {
@@ -1014,9 +2233,30 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["beach", "fashion", "female", "girl", "swimming", "woman"],
+ "keywords": [
+ "beach",
+ "fashion",
+ "female",
+ "girl",
+ "swimming",
+ "woman"
+ ],
"moji": "👙"
},
+ "biohazard": {
+ "unicode": "2623",
+ "unicode_alternates": "",
+ "name": "biohazard sign",
+ "shortname": ":biohazard:",
+ "category": "symbols",
+ "aliases": [
+ ":biohazard_sign:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "symbol"
+ ]
+ },
"bird": {
"unicode": "1F426",
"unicode_alternates": [],
@@ -1025,7 +2265,12 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "fly", "nature", "tweet"],
+ "keywords": [
+ "animal",
+ "fly",
+ "nature",
+ "tweet"
+ ],
"moji": "🐦"
},
"birthday": {
@@ -1036,18 +2281,31 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cake", "party", "birthday", "birth", "cake", "dessert", "wish", "celebrate"],
+ "keywords": [
+ "cake",
+ "party",
+ "birthday",
+ "birth",
+ "cake",
+ "dessert",
+ "wish",
+ "celebrate"
+ ],
"moji": "🎂"
},
"black_circle": {
"unicode": "26AB",
- "unicode_alternates": ["26AB-FE0F"],
+ "unicode_alternates": [
+ "26AB-FE0F"
+ ],
"name": "medium black circle",
"shortname": ":black_circle:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "⚫"
},
"black_joker": {
@@ -1058,23 +2316,33 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cards", "game", "poker"],
+ "keywords": [
+ "cards",
+ "game",
+ "poker"
+ ],
"moji": "🃏"
},
"black_large_square": {
"unicode": "2B1B",
- "unicode_alternates": ["2B1B-FE0F"],
+ "unicode_alternates": [
+ "2B1B-FE0F"
+ ],
"name": "black large square",
"shortname": ":black_large_square:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "⬛"
},
"black_medium_small_square": {
"unicode": "25FE",
- "unicode_alternates": ["25FE-FE0F"],
+ "unicode_alternates": [
+ "25FE-FE0F"
+ ],
"name": "black medium small square",
"shortname": ":black_medium_small_square:",
"category": "other",
@@ -1085,29 +2353,40 @@
},
"black_medium_square": {
"unicode": "25FC",
- "unicode_alternates": ["25FC-FE0F"],
+ "unicode_alternates": [
+ "25FC-FE0F"
+ ],
"name": "black medium square",
"shortname": ":black_medium_square:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "◼"
},
"black_nib": {
"unicode": "2712",
- "unicode_alternates": ["2712-FE0F"],
+ "unicode_alternates": [
+ "2712-FE0F"
+ ],
"name": "black nib",
"shortname": ":black_nib:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["pen", "stationery"],
+ "keywords": [
+ "pen",
+ "stationery"
+ ],
"moji": "✒"
},
"black_small_square": {
"unicode": "25AA",
- "unicode_alternates": ["25AA-FE0F"],
+ "unicode_alternates": [
+ "25AA-FE0F"
+ ],
"name": "black small square",
"shortname": ":black_small_square:",
"category": "other",
@@ -1124,7 +2403,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["frame"],
+ "keywords": [
+ "frame"
+ ],
"moji": "🔲"
},
"blossom": {
@@ -1135,7 +2416,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flowers", "nature", "yellow", "blossom", "daisy", "flower"],
+ "keywords": [
+ "flowers",
+ "nature",
+ "yellow",
+ "blossom",
+ "daisy",
+ "flower"
+ ],
"moji": "🌼"
},
"blowfish": {
@@ -1146,7 +2434,19 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "nature", "ocean", "sea", "blowfish", "pufferfish", "puffer", "ballonfish", "toadfish", "fugu fish", "sushi"],
+ "keywords": [
+ "food",
+ "nature",
+ "ocean",
+ "sea",
+ "blowfish",
+ "pufferfish",
+ "puffer",
+ "ballonfish",
+ "toadfish",
+ "fugu fish",
+ "sushi"
+ ],
"moji": "🐡"
},
"blue_book": {
@@ -1157,7 +2457,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["knowledge", "library", "read"],
+ "keywords": [
+ "knowledge",
+ "library",
+ "read"
+ ],
"moji": "📘"
},
"blue_car": {
@@ -1168,7 +2472,13 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["car", "suv", "car", "wagon", "automobile"],
+ "keywords": [
+ "car",
+ "suv",
+ "car",
+ "wagon",
+ "automobile"
+ ],
"moji": "🚙"
},
"blue_heart": {
@@ -1179,7 +2489,19 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines", "blue", "heart", "love", "stability", "truth", "loyalty", "trust"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines",
+ "blue",
+ "heart",
+ "love",
+ "stability",
+ "truth",
+ "loyalty",
+ "trust"
+ ],
"moji": "💙"
},
"blush": {
@@ -1190,7 +2512,18 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["crush", "embarrassed", "face", "flushed", "happy", "shy", "smile", "smiling", "smile", "smiley"],
+ "keywords": [
+ "crush",
+ "embarrassed",
+ "face",
+ "flushed",
+ "happy",
+ "shy",
+ "smile",
+ "smiling",
+ "smile",
+ "smiley"
+ ],
"moji": "😊"
},
"boar": {
@@ -1201,7 +2534,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐗"
},
"bomb": {
@@ -1212,7 +2548,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["boom", "explode"],
+ "keywords": [
+ "boom",
+ "explode"
+ ],
"moji": "💣"
},
"book": {
@@ -1223,7 +2562,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["library", "literature"],
+ "keywords": [
+ "library",
+ "literature"
+ ],
"moji": "📖"
},
"book2": {
@@ -1234,7 +2576,13 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["library", "literature", "novel", "reading", "story"]
+ "keywords": [
+ "library",
+ "literature",
+ "novel",
+ "reading",
+ "story"
+ ]
},
"bookmark": {
"unicode": "1F516",
@@ -1244,7 +2592,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["favorite"],
+ "keywords": [
+ "favorite"
+ ],
"moji": "🔖"
},
"bookmark_tabs": {
@@ -1255,7 +2605,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["favorite"],
+ "keywords": [
+ "favorite"
+ ],
"moji": "📑"
},
"books": {
@@ -1266,7 +2618,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["library", "literature"],
+ "keywords": [
+ "library",
+ "literature"
+ ],
"moji": "📚"
},
"boom": {
@@ -1277,7 +2632,18 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bomb", "explode", "explosion", "boom", "bang", "collision", "fire", "emphasis", "wow", "bam"],
+ "keywords": [
+ "bomb",
+ "explode",
+ "explosion",
+ "boom",
+ "bang",
+ "collision",
+ "fire",
+ "emphasis",
+ "wow",
+ "bam"
+ ],
"moji": "💥"
},
"boot": {
@@ -1288,7 +2654,10 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fashion", "shoes"],
+ "keywords": [
+ "fashion",
+ "shoes"
+ ],
"moji": "👢"
},
"bouquet": {
@@ -1299,7 +2668,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flowers", "nature"],
+ "keywords": [
+ "flowers",
+ "nature"
+ ],
"moji": "💐"
},
"bouquet2": {
@@ -1308,9 +2680,16 @@
"name": "bouquet of flowers",
"shortname": ":bouquet2:",
"category": "celebration",
- "aliases": [":bouquet_of_flowers:"],
+ "aliases": [
+ ":bouquet_of_flowers:"
+ ],
"aliases_ascii": [],
- "keywords": ["nature", "marriage", "wedding", "bride"]
+ "keywords": [
+ "nature",
+ "marriage",
+ "wedding",
+ "bride"
+ ]
},
"bow": {
"unicode": "1F647",
@@ -1320,9 +2699,120 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["boy", "male", "man", "sorry", "bow", "respect", "curtsy", "bend"],
+ "keywords": [
+ "boy",
+ "male",
+ "man",
+ "sorry",
+ "bow",
+ "respect",
+ "curtsy",
+ "bend"
+ ],
"moji": "🙇"
},
+ "bow_and_arrow": {
+ "unicode": "1F3F9",
+ "unicode_alternates": "",
+ "name": "bow and arrow",
+ "shortname": ":bow_and_arrow:",
+ "category": "activity",
+ "aliases": [
+ ":archery:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "bow_tone1": {
+ "unicode": "1F647-1F3FB",
+ "unicode_alternates": "",
+ "name": "person bowing deeply tone 1",
+ "shortname": ":bow_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "man",
+ "sorry",
+ "bow",
+ "respect",
+ "bend"
+ ]
+ },
+ "bow_tone2": {
+ "unicode": "1F647-1F3FC",
+ "unicode_alternates": "",
+ "name": "person bowing deeply tone 2",
+ "shortname": ":bow_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "man",
+ "sorry",
+ "bow",
+ "respect",
+ "bend"
+ ]
+ },
+ "bow_tone3": {
+ "unicode": "1F647-1F3FD",
+ "unicode_alternates": "",
+ "name": "person bowing deeply tone 3",
+ "shortname": ":bow_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "man",
+ "sorry",
+ "bow",
+ "respect",
+ "bend"
+ ]
+ },
+ "bow_tone4": {
+ "unicode": "1F647-1F3FE",
+ "unicode_alternates": "",
+ "name": "person bowing deeply tone 4",
+ "shortname": ":bow_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "man",
+ "sorry",
+ "bow",
+ "respect",
+ "bend"
+ ]
+ },
+ "bow_tone5": {
+ "unicode": "1F647-1F3FF",
+ "unicode_alternates": "",
+ "name": "person bowing deeply tone 5",
+ "shortname": ":bow_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "man",
+ "sorry",
+ "bow",
+ "respect",
+ "bend"
+ ]
+ },
"bowling": {
"unicode": "1F3B3",
"unicode_alternates": [],
@@ -1331,7 +2821,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fun", "play", "sports", "bowl", "bowling", "ball", "pin", "strike", "spare", "game"],
+ "keywords": [
+ "fun",
+ "play",
+ "sports",
+ "bowl",
+ "bowling",
+ "ball",
+ "pin",
+ "strike",
+ "spare",
+ "game"
+ ],
"moji": "🎳"
},
"boy": {
@@ -1342,9 +2843,83 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["guy", "male", "man"],
+ "keywords": [
+ "guy",
+ "male",
+ "man"
+ ],
"moji": "👦"
},
+ "boy_tone1": {
+ "unicode": "1F466-1F3FB",
+ "unicode_alternates": "",
+ "name": "boy tone 1",
+ "shortname": ":boy_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "kid",
+ "child"
+ ]
+ },
+ "boy_tone2": {
+ "unicode": "1F466-1F3FC",
+ "unicode_alternates": "",
+ "name": "boy tone 2",
+ "shortname": ":boy_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "kid",
+ "child"
+ ]
+ },
+ "boy_tone3": {
+ "unicode": "1F466-1F3FD",
+ "unicode_alternates": "",
+ "name": "boy tone 3",
+ "shortname": ":boy_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "kid",
+ "child"
+ ]
+ },
+ "boy_tone4": {
+ "unicode": "1F466-1F3FE",
+ "unicode_alternates": "",
+ "name": "boy tone 4",
+ "shortname": ":boy_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "kid",
+ "child"
+ ]
+ },
+ "boy_tone5": {
+ "unicode": "1F466-1F3FF",
+ "unicode_alternates": "",
+ "name": "boy tone 5",
+ "shortname": ":boy_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "kid",
+ "child"
+ ]
+ },
"boys_symbol": {
"unicode": "1F6C9",
"unicode_alternates": [],
@@ -1353,7 +2928,10 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["male", "child"]
+ "keywords": [
+ "male",
+ "child"
+ ]
},
"bread": {
"unicode": "1F35E",
@@ -1363,7 +2941,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["breakfast", "food", "toast", "wheat", "bread", "loaf", "yeast"],
+ "keywords": [
+ "breakfast",
+ "food",
+ "toast",
+ "wheat",
+ "bread",
+ "loaf",
+ "yeast"
+ ],
"moji": "🍞"
},
"bride_with_veil": {
@@ -1374,9 +2960,121 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["couple", "marriage", "wedding", "bride", "wedding", "planning", "veil", "gown", "dress", "engagement", "white"],
+ "keywords": [
+ "couple",
+ "marriage",
+ "wedding",
+ "bride",
+ "wedding",
+ "planning",
+ "veil",
+ "gown",
+ "dress",
+ "engagement",
+ "white"
+ ],
"moji": "👰"
},
+ "bride_with_veil_tone1": {
+ "unicode": "1F470-1F3FB",
+ "unicode_alternates": "",
+ "name": "bride with veil tone 1",
+ "shortname": ":bride_with_veil_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "couple",
+ "marriage",
+ "wedding",
+ "wedding",
+ "planning",
+ "gown",
+ "dress",
+ "engagement",
+ "white"
+ ]
+ },
+ "bride_with_veil_tone2": {
+ "unicode": "1F470-1F3FC",
+ "unicode_alternates": "",
+ "name": "bride with veil tone 2",
+ "shortname": ":bride_with_veil_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "couple",
+ "marriage",
+ "wedding",
+ "wedding",
+ "planning",
+ "gown",
+ "dress",
+ "engagement",
+ "white"
+ ]
+ },
+ "bride_with_veil_tone3": {
+ "unicode": "1F470-1F3FD",
+ "unicode_alternates": "",
+ "name": "bride with veil tone 3",
+ "shortname": ":bride_with_veil_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "couple",
+ "marriage",
+ "wedding",
+ "wedding",
+ "planning",
+ "gown",
+ "dress",
+ "engagement",
+ "white"
+ ]
+ },
+ "bride_with_veil_tone4": {
+ "unicode": "1F470-1F3FE",
+ "unicode_alternates": "",
+ "name": "bride with veil tone 4",
+ "shortname": ":bride_with_veil_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "couple",
+ "marriage",
+ "wedding",
+ "wedding",
+ "planning",
+ "gown",
+ "dress",
+ "engagement",
+ "white"
+ ]
+ },
+ "bride_with_veil_tone5": {
+ "unicode": "1F470-1F3FF",
+ "unicode_alternates": "",
+ "name": "bride with veil tone 5",
+ "shortname": ":bride_with_veil_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "couple",
+ "marriage",
+ "wedding",
+ "wedding",
+ "planning",
+ "gown",
+ "dress",
+ "engagement",
+ "white"
+ ]
+ },
"bridge_at_night": {
"unicode": "1F309",
"unicode_alternates": [],
@@ -1385,7 +3083,18 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["photo", "sanfrancisco", "bridge", "night", "water", "road", "evening", "suspension", "golden", "gate"],
+ "keywords": [
+ "photo",
+ "sanfrancisco",
+ "bridge",
+ "night",
+ "water",
+ "road",
+ "evening",
+ "suspension",
+ "golden",
+ "gate"
+ ],
"moji": "🌉"
},
"briefcase": {
@@ -1396,7 +3105,11 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["business", "documents", "work"],
+ "keywords": [
+ "business",
+ "documents",
+ "work"
+ ],
"moji": "💼"
},
"broken_heart": {
@@ -1406,8 +3119,13 @@
"shortname": ":broken_heart:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["</3"],
- "keywords": ["sad", "sorry"],
+ "aliases_ascii": [
+ "</3"
+ ],
+ "keywords": [
+ "sad",
+ "sorry"
+ ],
"moji": "💔"
},
"bug": {
@@ -1418,7 +3136,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["insect", "nature", "bug", "insect", "virus", "error"],
+ "keywords": [
+ "insect",
+ "nature",
+ "bug",
+ "insect",
+ "virus",
+ "error"
+ ],
"moji": "🐛"
},
"bulb": {
@@ -1429,7 +3154,13 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["electricity", "light", "idea", "bulb", "light"],
+ "keywords": [
+ "electricity",
+ "light",
+ "idea",
+ "bulb",
+ "light"
+ ],
"moji": "💡"
},
"bullettrain_front": {
@@ -1440,7 +3171,12 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "train", "bullet", "rail"],
+ "keywords": [
+ "transportation",
+ "train",
+ "bullet",
+ "rail"
+ ],
"moji": "🚅"
},
"bullettrain_side": {
@@ -1451,7 +3187,13 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "train", "bullet", "rail"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "train",
+ "bullet",
+ "rail"
+ ],
"moji": "🚄"
},
"bullhorn": {
@@ -1462,7 +3204,12 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sound", "noise", "announcement", "megaphone"]
+ "keywords": [
+ "sound",
+ "noise",
+ "announcement",
+ "megaphone"
+ ]
},
"bullhorn_waves": {
"unicode": "1F56C",
@@ -1470,9 +3217,26 @@
"name": "bullhorn with sound waves",
"shortname": ":bullhorn_waves:",
"category": "objects_symbols",
- "aliases": [":bullhorn_with_sound_waves:"],
+ "aliases": [
+ ":bullhorn_with_sound_waves:"
+ ],
"aliases_ascii": [],
- "keywords": ["sound", "noise", "announcement", "megaphone"]
+ "keywords": [
+ "sound",
+ "noise",
+ "announcement",
+ "megaphone"
+ ]
+ },
+ "burrito": {
+ "unicode": "1F32F",
+ "unicode_alternates": "",
+ "name": "burrito",
+ "shortname": ":burrito:",
+ "category": "foods",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
},
"bus": {
"unicode": "1F68C",
@@ -1482,7 +3246,16 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["car", "transportation", "vehicle", "bus", "school", "city", "transportation", "public"],
+ "keywords": [
+ "car",
+ "transportation",
+ "vehicle",
+ "bus",
+ "school",
+ "city",
+ "transportation",
+ "public"
+ ],
"moji": "🚌"
},
"busstop": {
@@ -1493,7 +3266,14 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "bus", "stop", "city", "transport", "transportation"],
+ "keywords": [
+ "transportation",
+ "bus",
+ "stop",
+ "city",
+ "transport",
+ "transportation"
+ ],
"moji": "🚏"
},
"bust_in_silhouette": {
@@ -1504,7 +3284,24 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["human", "man", "person", "user", "silhouette", "person", "user", "member", "account", "guest", "icon", "avatar", "profile", "me", "myself", "i"],
+ "keywords": [
+ "human",
+ "man",
+ "person",
+ "user",
+ "silhouette",
+ "person",
+ "user",
+ "member",
+ "account",
+ "guest",
+ "icon",
+ "avatar",
+ "profile",
+ "me",
+ "myself",
+ "i"
+ ],
"moji": "👤"
},
"busts_in_silhouette": {
@@ -1515,7 +3312,22 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["group", "human", "man", "person", "team", "user", "silhouette", "silhouettes", "people", "user", "members", "accounts", "relationship", "shadow"],
+ "keywords": [
+ "group",
+ "human",
+ "man",
+ "person",
+ "team",
+ "user",
+ "silhouette",
+ "silhouettes",
+ "people",
+ "user",
+ "members",
+ "accounts",
+ "relationship",
+ "shadow"
+ ],
"moji": "👥"
},
"cactus": {
@@ -1526,7 +3338,16 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "plant", "vegetable", "cactus", "desert", "drought", "spike", "poke"],
+ "keywords": [
+ "nature",
+ "plant",
+ "vegetable",
+ "cactus",
+ "desert",
+ "drought",
+ "spike",
+ "poke"
+ ],
"moji": "🌵"
},
"cake": {
@@ -1537,7 +3358,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["desert", "food", "cake", "short", "dessert", "strawberry"],
+ "keywords": [
+ "desert",
+ "food",
+ "cake",
+ "short",
+ "dessert",
+ "strawberry"
+ ],
"moji": "🍰"
},
"calculator": {
@@ -1546,9 +3374,17 @@
"name": "pocket calculator",
"shortname": ":calculator:",
"category": "objects_symbols",
- "aliases": [":pocket calculator:"],
- "aliases_ascii": [],
- "keywords": ["add", "subtract", "multiple", "divide", "scientific"]
+ "aliases": [
+ ":pocket calculator:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "add",
+ "subtract",
+ "multiple",
+ "divide",
+ "scientific"
+ ]
},
"calendar": {
"unicode": "1F4C6",
@@ -1558,7 +3394,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["schedule"],
+ "keywords": [
+ "schedule"
+ ],
"moji": "📆"
},
"calendar_spiral": {
@@ -1567,9 +3405,15 @@
"name": "spiral calendar pad",
"shortname": ":calendar_spiral:",
"category": "objects_symbols",
- "aliases": [":spiral_calendar_pad:"],
+ "aliases": [
+ ":spiral_calendar_pad:"
+ ],
"aliases_ascii": [],
- "keywords": ["schedule", "date", "day"]
+ "keywords": [
+ "schedule",
+ "date",
+ "day"
+ ]
},
"calling": {
"unicode": "1F4F2",
@@ -1579,7 +3423,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["incoming", "iphone"],
+ "keywords": [
+ "incoming",
+ "iphone"
+ ],
"moji": "📲"
},
"camel": {
@@ -1590,7 +3437,22 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "hot", "nature", "bactrian", "camel", "hump", "desert", "central asia", "heat", "hot", "water", "hump day", "wednesday", "sex"],
+ "keywords": [
+ "animal",
+ "hot",
+ "nature",
+ "bactrian",
+ "camel",
+ "hump",
+ "desert",
+ "central asia",
+ "heat",
+ "hot",
+ "water",
+ "hump day",
+ "wednesday",
+ "sex"
+ ],
"moji": "🐫"
},
"camera": {
@@ -1601,7 +3463,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["gadgets", "photo"],
+ "keywords": [
+ "gadgets",
+ "photo"
+ ],
"moji": "📷"
},
"camera_with_flash": {
@@ -1612,7 +3477,10 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["photo", "picture"]
+ "keywords": [
+ "photo",
+ "picture"
+ ]
},
"camping": {
"unicode": "1F3D5",
@@ -1622,7 +3490,13 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["outdoors", "nature", "wilderness", "roughing", "activity"]
+ "keywords": [
+ "outdoors",
+ "nature",
+ "wilderness",
+ "roughing",
+ "activity"
+ ]
},
"cancellation_x": {
"unicode": "1F5D9",
@@ -1632,17 +3506,35 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cancel", "stop", "delete"]
+ "keywords": [
+ "cancel",
+ "stop",
+ "delete"
+ ]
},
"cancer": {
"unicode": "264B",
- "unicode_alternates": ["264B-FE0F"],
+ "unicode_alternates": [
+ "264B-FE0F"
+ ],
"name": "cancer",
"shortname": ":cancer:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cancer", "crab", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "cancer",
+ "crab",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♋"
},
"candle": {
@@ -1653,7 +3545,10 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["light", "wax"]
+ "keywords": [
+ "light",
+ "wax"
+ ]
},
"candy": {
"unicode": "1F36C",
@@ -1663,7 +3558,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["desert", "snack", "candy", "sugar", "sweet", "hard"],
+ "keywords": [
+ "desert",
+ "snack",
+ "candy",
+ "sugar",
+ "sweet",
+ "hard"
+ ],
"moji": "🍬"
},
"capital_abcd": {
@@ -1674,18 +3576,37 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "blue-square", "words"],
+ "keywords": [
+ "alphabet",
+ "blue-square",
+ "words"
+ ],
"moji": "🔠"
},
"capricorn": {
"unicode": "2651",
- "unicode_alternates": ["2651-FE0F"],
+ "unicode_alternates": [
+ "2651-FE0F"
+ ],
"name": "capricorn",
"shortname": ":capricorn:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["capricorn", "sea-goat", "goat-horned", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "capricorn",
+ "sea-goat",
+ "goat-horned",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♑"
},
"card_box": {
@@ -1694,9 +3615,14 @@
"name": "card file box",
"shortname": ":card_box:",
"category": "objects_symbols",
- "aliases": [":card_file_box:"],
+ "aliases": [
+ ":card_file_box:"
+ ],
"aliases_ascii": [],
- "keywords": ["index", "organization"]
+ "keywords": [
+ "index",
+ "organization"
+ ]
},
"card_index": {
"unicode": "1F4C7",
@@ -1706,7 +3632,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["business", "stationery"],
+ "keywords": [
+ "business",
+ "stationery"
+ ],
"moji": "📇"
},
"carousel_horse": {
@@ -1717,7 +3646,19 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["carnival", "horse", "photo", "carousel", "horse", "amusement", "park", "ride", "entertainment", "park", "fair"],
+ "keywords": [
+ "carnival",
+ "horse",
+ "photo",
+ "carousel",
+ "horse",
+ "amusement",
+ "park",
+ "ride",
+ "entertainment",
+ "park",
+ "fair"
+ ],
"moji": "🎠"
},
"cartridge": {
@@ -1726,9 +3667,21 @@
"name": "tape cartridge",
"shortname": ":cartridge:",
"category": "objects_symbols",
- "aliases": [":tape_cartridge:"],
- "aliases_ascii": [],
- "keywords": ["oldschool", "save", "technology", "disk", "storage", "information", "computer", "drive", "megabyte"]
+ "aliases": [
+ ":tape_cartridge:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "oldschool",
+ "save",
+ "technology",
+ "disk",
+ "storage",
+ "information",
+ "computer",
+ "drive",
+ "megabyte"
+ ]
},
"cat": {
"unicode": "1F431",
@@ -1738,7 +3691,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "meow"],
+ "keywords": [
+ "animal",
+ "meow"
+ ],
"moji": "🐱"
},
"cat2": {
@@ -1749,9 +3705,36 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "meow", "pet", "cat", "kitten", "meow"],
+ "keywords": [
+ "animal",
+ "meow",
+ "pet",
+ "cat",
+ "kitten",
+ "meow"
+ ],
"moji": "🐈"
},
+ "cd": {
+ "unicode": "1F4BF",
+ "unicode_alternates": "",
+ "name": "optical disc",
+ "shortname": ":cd:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "disc",
+ "disk",
+ "dvd",
+ "technology",
+ "blu-ray",
+ "cd",
+ "computer",
+ "object",
+ "office"
+ ]
+ },
"celtic_cross": {
"unicode": "1F548",
"unicode_alternates": [],
@@ -1760,7 +3743,35 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["religion", "symbol"]
+ "keywords": [
+ "religion",
+ "symbol"
+ ]
+ },
+ "chains": {
+ "unicode": "26D3",
+ "unicode_alternates": "",
+ "name": "chains",
+ "shortname": ":chains:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "chain",
+ "object"
+ ]
+ },
+ "champagne": {
+ "unicode": "1F37E",
+ "unicode_alternates": "",
+ "name": "bottle with popping cork",
+ "shortname": ":champagne:",
+ "category": "foods",
+ "aliases": [
+ ":bottle_with_popping_cork:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
},
"chart": {
"unicode": "1F4B9",
@@ -1770,7 +3781,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["graph", "green-square"],
+ "keywords": [
+ "graph",
+ "green-square"
+ ],
"moji": "💹"
},
"chart_with_downwards_trend": {
@@ -1781,7 +3795,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["graph"],
+ "keywords": [
+ "graph"
+ ],
"moji": "📉"
},
"chart_with_upwards_trend": {
@@ -1792,7 +3808,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["graph"],
+ "keywords": [
+ "graph"
+ ],
"moji": "📈"
},
"checkered_flag": {
@@ -1803,9 +3821,33 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["contest", "finishline", "gokart", "rase", "checkered", "chequred", "race", "flag", "finish", "complete", "end"],
+ "keywords": [
+ "contest",
+ "finishline",
+ "gokart",
+ "rase",
+ "checkered",
+ "chequred",
+ "race",
+ "flag",
+ "finish",
+ "complete",
+ "end"
+ ],
"moji": "🏁"
},
+ "cheese": {
+ "unicode": "1F9C0",
+ "unicode_alternates": "",
+ "name": "cheese wedge",
+ "shortname": ":cheese:",
+ "category": "foods",
+ "aliases": [
+ ":cheese_wedge:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"cherries": {
"unicode": "1F352",
"unicode_alternates": [],
@@ -1814,7 +3856,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "cherry", "cherries", "tree", "fruit", "pit"],
+ "keywords": [
+ "food",
+ "fruit",
+ "cherry",
+ "cherries",
+ "tree",
+ "fruit",
+ "pit"
+ ],
"moji": "🍒"
},
"cherry_blossom": {
@@ -1825,7 +3875,15 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flower", "nature", "plant", "cherry", "blossom", "tree", "flower"],
+ "keywords": [
+ "flower",
+ "nature",
+ "plant",
+ "cherry",
+ "blossom",
+ "tree",
+ "flower"
+ ],
"moji": "🌸"
},
"chestnut": {
@@ -1836,7 +3894,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "squirrel", "chestnut", "roasted", "food", "tree"],
+ "keywords": [
+ "food",
+ "squirrel",
+ "chestnut",
+ "roasted",
+ "food",
+ "tree"
+ ],
"moji": "🌰"
},
"chicken": {
@@ -1847,7 +3912,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cluck", "chicken", "hen", "poultry", "livestock"],
+ "keywords": [
+ "animal",
+ "cluck",
+ "chicken",
+ "hen",
+ "poultry",
+ "livestock"
+ ],
"moji": "🐔"
},
"children_crossing": {
@@ -1858,7 +3930,16 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["school", "children", "kids", "caution", "crossing", "street", "crosswalk", "slow"],
+ "keywords": [
+ "school",
+ "children",
+ "kids",
+ "caution",
+ "crossing",
+ "street",
+ "crosswalk",
+ "slow"
+ ],
"moji": "🚸"
},
"chipmunk": {
@@ -1869,7 +3950,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"]
+ "keywords": [
+ "animal",
+ "nature"
+ ]
},
"chocolate_bar": {
"unicode": "1F36B",
@@ -1879,7 +3963,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["desert", "food", "snack", "chocolate", "bar", "candy", "coca", "hershey&#039;s"],
+ "keywords": [
+ "desert",
+ "food",
+ "snack",
+ "chocolate",
+ "bar",
+ "candy",
+ "coca",
+ "hershey&#039;s"
+ ],
"moji": "🍫"
},
"christmas_tree": {
@@ -1890,18 +3983,42 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["celebration", "december", "festival", "vacation", "xmas", "christmas", "xmas", "santa", "holiday", "winter", "december", "santa", "evergreen", "ornaments", "jesus", "gifts", "presents"],
+ "keywords": [
+ "celebration",
+ "december",
+ "festival",
+ "vacation",
+ "xmas",
+ "christmas",
+ "xmas",
+ "santa",
+ "holiday",
+ "winter",
+ "december",
+ "santa",
+ "evergreen",
+ "ornaments",
+ "jesus",
+ "gifts",
+ "presents"
+ ],
"moji": "🎄"
},
"church": {
"unicode": "26EA",
- "unicode_alternates": ["26EA-FE0F"],
+ "unicode_alternates": [
+ "26EA-FE0F"
+ ],
"name": "church",
"shortname": ":church:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "christ", "religion"],
+ "keywords": [
+ "building",
+ "christ",
+ "religion"
+ ],
"moji": "⛪"
},
"cinema": {
@@ -1912,7 +4029,17 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "film", "movie", "record", "cinema", "movie", "theater", "motion", "picture"],
+ "keywords": [
+ "blue-square",
+ "film",
+ "movie",
+ "record",
+ "cinema",
+ "movie",
+ "theater",
+ "motion",
+ "picture"
+ ],
"moji": "🎦"
},
"circus_tent": {
@@ -1923,7 +4050,18 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["carnival", "festival", "party", "circus", "tent", "event", "carnival", "big", "top", "canvas"],
+ "keywords": [
+ "carnival",
+ "festival",
+ "party",
+ "circus",
+ "tent",
+ "event",
+ "carnival",
+ "big",
+ "top",
+ "canvas"
+ ],
"moji": "🎪"
},
"city_dusk": {
@@ -1934,7 +4072,18 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["photo", "city", "scape", "sunset", "dusk", "lights", "evening", "metropolitan", "night", "dark"],
+ "keywords": [
+ "photo",
+ "city",
+ "scape",
+ "sunset",
+ "dusk",
+ "lights",
+ "evening",
+ "metropolitan",
+ "night",
+ "dark"
+ ],
"moji": "🌆"
},
"city_sunset": {
@@ -1943,9 +4092,22 @@
"name": "sunset over buildings",
"shortname": ":city_sunset:",
"category": "places",
- "aliases": [":city_sunrise:"],
- "aliases_ascii": [],
- "keywords": ["photo", "city", "scape", "sunrise", "dawn", "light", "morning", "metropolitan", "rise", "sun"],
+ "aliases": [
+ ":city_sunrise:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "photo",
+ "city",
+ "scape",
+ "sunrise",
+ "dawn",
+ "light",
+ "morning",
+ "metropolitan",
+ "rise",
+ "sun"
+ ],
"moji": "🌇"
},
"cityscape": {
@@ -1956,7 +4118,32 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["skyscraper", "city", "view", "lights", "buiildings", "metropolis"]
+ "keywords": [
+ "skyscraper",
+ "city",
+ "view",
+ "lights",
+ "buiildings",
+ "metropolis"
+ ]
+ },
+ "cl": {
+ "unicode": "1F191",
+ "unicode_alternates": "",
+ "name": "squared cl",
+ "shortname": ":cl:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "alphabet",
+ "red-square",
+ "words",
+ "cl",
+ "clear",
+ "symbol",
+ "word"
+ ]
},
"clap": {
"unicode": "1F44F",
@@ -1966,9 +4153,120 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["applause", "congrats", "hands", "praise", "clapping", "appreciation", "approval", "sound", "encouragement", "enthusiasm"],
+ "keywords": [
+ "applause",
+ "congrats",
+ "hands",
+ "praise",
+ "clapping",
+ "appreciation",
+ "approval",
+ "sound",
+ "encouragement",
+ "enthusiasm"
+ ],
"moji": "👏"
},
+ "clap_tone1": {
+ "unicode": "1F44F-1F3FB",
+ "unicode_alternates": "",
+ "name": "clapping hands sign tone 1",
+ "shortname": ":clap_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "applause",
+ "congrats",
+ "praise",
+ "clap",
+ "appreciation",
+ "approval",
+ "sound",
+ "encouragement",
+ "enthusiasm"
+ ]
+ },
+ "clap_tone2": {
+ "unicode": "1F44F-1F3FC",
+ "unicode_alternates": "",
+ "name": "clapping hands sign tone 2",
+ "shortname": ":clap_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "applause",
+ "congrats",
+ "praise",
+ "clap",
+ "appreciation",
+ "approval",
+ "sound",
+ "encouragement",
+ "enthusiasm"
+ ]
+ },
+ "clap_tone3": {
+ "unicode": "1F44F-1F3FD",
+ "unicode_alternates": "",
+ "name": "clapping hands sign tone 3",
+ "shortname": ":clap_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "applause",
+ "congrats",
+ "praise",
+ "clap",
+ "appreciation",
+ "approval",
+ "sound",
+ "encouragement",
+ "enthusiasm"
+ ]
+ },
+ "clap_tone4": {
+ "unicode": "1F44F-1F3FE",
+ "unicode_alternates": "",
+ "name": "clapping hands sign tone 4",
+ "shortname": ":clap_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "applause",
+ "congrats",
+ "praise",
+ "clap",
+ "appreciation",
+ "approval",
+ "sound",
+ "encouragement",
+ "enthusiasm"
+ ]
+ },
+ "clap_tone5": {
+ "unicode": "1F44F-1F3FF",
+ "unicode_alternates": "",
+ "name": "clapping hands sign tone 5",
+ "shortname": ":clap_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "applause",
+ "congrats",
+ "praise",
+ "clap",
+ "appreciation",
+ "approval",
+ "sound",
+ "encouragement",
+ "enthusiasm"
+ ]
+ },
"clapper": {
"unicode": "1F3AC",
"unicode_alternates": [],
@@ -1977,7 +4275,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["film", "movie", "record", "clapper", "board", "clapboard", "movie", "film", "take"],
+ "keywords": [
+ "film",
+ "movie",
+ "record",
+ "clapper",
+ "board",
+ "clapboard",
+ "movie",
+ "film",
+ "take"
+ ],
"moji": "🎬"
},
"classical_building": {
@@ -1988,7 +4296,13 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["government", "architecture", "history", "iconic", "genre"]
+ "keywords": [
+ "government",
+ "architecture",
+ "history",
+ "iconic",
+ "genre"
+ ]
},
"clipboard": {
"unicode": "1F4CB",
@@ -1998,7 +4312,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents", "stationery"],
+ "keywords": [
+ "documents",
+ "stationery"
+ ],
"moji": "📋"
},
"clock": {
@@ -2007,9 +4324,13 @@
"name": "mantlepiece clock",
"shortname": ":clock:",
"category": "objects_symbols",
- "aliases": [":mantlepiece_clock:"],
+ "aliases": [
+ ":mantlepiece_clock:"
+ ],
"aliases_ascii": [],
- "keywords": ["time"]
+ "keywords": [
+ "time"
+ ]
},
"clock1": {
"unicode": "1F550",
@@ -2019,7 +4340,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕐"
},
"clock10": {
@@ -2030,7 +4354,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕙"
},
"clock1030": {
@@ -2041,7 +4368,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕥"
},
"clock11": {
@@ -2052,7 +4382,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕚"
},
"clock1130": {
@@ -2063,7 +4396,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕦"
},
"clock12": {
@@ -2074,7 +4410,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕛"
},
"clock1230": {
@@ -2085,7 +4424,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"]
+ "keywords": [
+ "clock",
+ "time"
+ ]
},
"clock130": {
"unicode": "1F55C",
@@ -2095,7 +4437,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕜"
},
"clock2": {
@@ -2106,7 +4451,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕑"
},
"clock230": {
@@ -2117,7 +4465,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕝"
},
"clock3": {
@@ -2128,7 +4479,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕒"
},
"clock330": {
@@ -2139,7 +4493,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕞"
},
"clock4": {
@@ -2150,7 +4507,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕓"
},
"clock430": {
@@ -2161,7 +4521,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕟"
},
"clock5": {
@@ -2172,7 +4535,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕔"
},
"clock530": {
@@ -2183,7 +4549,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕠"
},
"clock6": {
@@ -2194,7 +4563,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕕"
},
"clock630": {
@@ -2205,7 +4577,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕡"
},
"clock7": {
@@ -2216,7 +4591,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕖"
},
"clock730": {
@@ -2227,7 +4605,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕢"
},
"clock8": {
@@ -2238,7 +4619,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕗"
},
"clock830": {
@@ -2249,7 +4633,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕣"
},
"clock9": {
@@ -2260,7 +4647,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕘"
},
"clock930": {
@@ -2271,7 +4661,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "time"],
+ "keywords": [
+ "clock",
+ "time"
+ ],
"moji": "🕤"
},
"clockwise_arrows": {
@@ -2280,9 +4673,13 @@
"name": "clockwise right and left semicircle arrows",
"shortname": ":clockwise_arrows:",
"category": "objects_symbols",
- "aliases": [":clockwise_right_and_left_semicircle_arrows:"],
+ "aliases": [
+ ":clockwise_right_and_left_semicircle_arrows:"
+ ],
"aliases_ascii": [],
- "keywords": ["sync"]
+ "keywords": [
+ "sync"
+ ]
},
"closed_book": {
"unicode": "1F4D5",
@@ -2292,7 +4689,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["knowledge", "library", "read"],
+ "keywords": [
+ "knowledge",
+ "library",
+ "read"
+ ],
"moji": "📕"
},
"closed_lock_with_key": {
@@ -2303,7 +4704,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["privacy", "security"],
+ "keywords": [
+ "privacy",
+ "security"
+ ],
"moji": "🔐"
},
"closed_umbrella": {
@@ -2314,18 +4718,35 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["drizzle", "rain", "weather", "umbrella", "closed", "rain", "moisture", "protection", "sun", "ultraviolet", "uv"],
+ "keywords": [
+ "drizzle",
+ "rain",
+ "weather",
+ "umbrella",
+ "closed",
+ "rain",
+ "moisture",
+ "protection",
+ "sun",
+ "ultraviolet",
+ "uv"
+ ],
"moji": "🌂"
},
"cloud": {
"unicode": "2601",
- "unicode_alternates": ["2601-FE0F"],
+ "unicode_alternates": [
+ "2601-FE0F"
+ ],
"name": "cloud",
"shortname": ":cloud:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sky", "weather"],
+ "keywords": [
+ "sky",
+ "weather"
+ ],
"moji": "☁"
},
"cloud_lightning": {
@@ -2334,9 +4755,14 @@
"name": "cloud with lightning",
"shortname": ":cloud_lightning:",
"category": "nature",
- "aliases": [":cloud_with_lightning:"],
+ "aliases": [
+ ":cloud_with_lightning:"
+ ],
"aliases_ascii": [],
- "keywords": ["weather", "thunder"]
+ "keywords": [
+ "weather",
+ "thunder"
+ ]
},
"cloud_rain": {
"unicode": "1F327",
@@ -2344,9 +4770,14 @@
"name": "cloud with rain",
"shortname": ":cloud_rain:",
"category": "nature",
- "aliases": [":cloud_with_rain:"],
+ "aliases": [
+ ":cloud_with_rain:"
+ ],
"aliases_ascii": [],
- "keywords": ["weather", "wet"]
+ "keywords": [
+ "weather",
+ "wet"
+ ]
},
"cloud_snow": {
"unicode": "1F328",
@@ -2354,9 +4785,14 @@
"name": "cloud with snow",
"shortname": ":cloud_snow:",
"category": "nature",
- "aliases": [":cloud_with_snow:"],
+ "aliases": [
+ ":cloud_with_snow:"
+ ],
"aliases_ascii": [],
- "keywords": ["weather", "cold"]
+ "keywords": [
+ "weather",
+ "cold"
+ ]
},
"cloud_tornado": {
"unicode": "1F32A",
@@ -2364,19 +4800,30 @@
"name": "cloud with tornado",
"shortname": ":cloud_tornado:",
"category": "nature",
- "aliases": [":cloud_with_tornado:"],
+ "aliases": [
+ ":cloud_with_tornado:"
+ ],
"aliases_ascii": [],
- "keywords": ["weather", "destruction", "funnel"]
+ "keywords": [
+ "weather",
+ "destruction",
+ "funnel"
+ ]
},
"clubs": {
"unicode": "2663",
- "unicode_alternates": ["2663-FE0F"],
+ "unicode_alternates": [
+ "2663-FE0F"
+ ],
"name": "black club suit",
"shortname": ":clubs:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cards", "poker"],
+ "keywords": [
+ "cards",
+ "poker"
+ ],
"moji": "♣"
},
"cocktail": {
@@ -2387,20 +4834,52 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alcohol", "beverage", "drink", "drunk", "cocktail", "mixed", "drink", "alcohol", "glass", "martini", "bar"],
+ "keywords": [
+ "alcohol",
+ "beverage",
+ "drink",
+ "drunk",
+ "cocktail",
+ "mixed",
+ "drink",
+ "alcohol",
+ "glass",
+ "martini",
+ "bar"
+ ],
"moji": "🍸"
},
"coffee": {
"unicode": "2615",
- "unicode_alternates": ["2615-FE0F"],
+ "unicode_alternates": [
+ "2615-FE0F"
+ ],
"name": "hot beverage",
"shortname": ":coffee:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["beverage", "cafe", "drink", "espresso"],
+ "keywords": [
+ "beverage",
+ "cafe",
+ "drink",
+ "espresso"
+ ],
"moji": "☕"
},
+ "coffin": {
+ "unicode": "26B0",
+ "unicode_alternates": "",
+ "name": "coffin",
+ "shortname": ":coffin:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "death",
+ "object"
+ ]
+ },
"cold_sweat": {
"unicode": "1F630",
"unicode_alternates": [],
@@ -2409,9 +4888,28 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "nervous", "sweat", "exasperated", "frustrated"],
+ "keywords": [
+ "face",
+ "nervous",
+ "sweat",
+ "exasperated",
+ "frustrated"
+ ],
"moji": "😰"
},
+ "comet": {
+ "unicode": "2604",
+ "unicode_alternates": "",
+ "name": "comet",
+ "shortname": ":comet:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "object",
+ "space"
+ ]
+ },
"compression": {
"unicode": "1F5DC",
"unicode_alternates": [],
@@ -2420,7 +4918,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["reduce"]
+ "keywords": [
+ "reduce"
+ ]
},
"computer": {
"unicode": "1F4BB",
@@ -2430,7 +4930,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["laptop", "tech"],
+ "keywords": [
+ "laptop",
+ "tech"
+ ],
"moji": "💻"
},
"computer_old": {
@@ -2439,9 +4942,14 @@
"name": "old personal computer",
"shortname": ":computer_old:",
"category": "objects_symbols",
- "aliases": [":old_personal_computer:"],
+ "aliases": [
+ ":old_personal_computer:"
+ ],
"aliases_ascii": [],
- "keywords": ["cpu", "terminal"]
+ "keywords": [
+ "cpu",
+ "terminal"
+ ]
},
"confetti_ball": {
"unicode": "1F38A",
@@ -2451,7 +4959,19 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["festival", "party", "party", "congratulations", "confetti", "ball", "celebrate", "win", "birthday", "new years", "wedding"],
+ "keywords": [
+ "festival",
+ "party",
+ "party",
+ "congratulations",
+ "confetti",
+ "ball",
+ "celebrate",
+ "win",
+ "birthday",
+ "new years",
+ "wedding"
+ ],
"moji": "🎊"
},
"confounded": {
@@ -2462,7 +4982,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["confused", "face", "sick", "unwell", "confound", "amaze", "perplex", "puzzle", "mystify"],
+ "keywords": [
+ "confused",
+ "face",
+ "sick",
+ "unwell",
+ "confound",
+ "amaze",
+ "perplex",
+ "puzzle",
+ "mystify"
+ ],
"moji": "😖"
},
"confused": {
@@ -2472,19 +5002,47 @@
"shortname": ":confused:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [">:\\", ">:/", ":-/", ":-.", ":/", ":\\", "=/", "=\\", ":L", "=L"],
- "keywords": ["confused", "confuse", "daze", "perplex", "puzzle", "indifference", "skeptical", "undecided", "uneasy", "hesitant"],
+ "aliases_ascii": [
+ ">:\\",
+ ">:/",
+ ":-/",
+ ":-.",
+ ":/",
+ ":\\",
+ "=/",
+ "=\\",
+ ":L",
+ "=L"
+ ],
+ "keywords": [
+ "confused",
+ "confuse",
+ "daze",
+ "perplex",
+ "puzzle",
+ "indifference",
+ "skeptical",
+ "undecided",
+ "uneasy",
+ "hesitant"
+ ],
"moji": "😕"
},
"congratulations": {
"unicode": "3297",
- "unicode_alternates": ["3297-FE0F"],
+ "unicode_alternates": [
+ "3297-FE0F"
+ ],
"name": "circled ideograph congratulation",
"shortname": ":congratulations:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "japanese", "kanji"],
+ "keywords": [
+ "chinese",
+ "japanese",
+ "kanji"
+ ],
"moji": "㊗"
},
"construction": {
@@ -2495,9 +5053,29 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["caution", "progress", "wip"],
+ "keywords": [
+ "caution",
+ "progress",
+ "wip"
+ ],
"moji": "🚧"
},
+ "construction_site": {
+ "unicode": "1F3D7",
+ "unicode_alternates": "",
+ "name": "building construction",
+ "shortname": ":construction_site:",
+ "category": "travel",
+ "aliases": [
+ ":building_construction:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "site",
+ "work",
+ "place"
+ ]
+ },
"construction_worker": {
"unicode": "1F477",
"unicode_alternates": [],
@@ -2506,9 +5084,89 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["human", "male", "man", "wip"],
+ "keywords": [
+ "human",
+ "male",
+ "man",
+ "wip"
+ ],
"moji": "👷"
},
+ "construction_worker_tone1": {
+ "unicode": "1F477-1F3FB",
+ "unicode_alternates": "",
+ "name": "construction worker tone 1",
+ "shortname": ":construction_worker_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "human",
+ "male",
+ "man",
+ "wip"
+ ]
+ },
+ "construction_worker_tone2": {
+ "unicode": "1F477-1F3FC",
+ "unicode_alternates": "",
+ "name": "construction worker tone 2",
+ "shortname": ":construction_worker_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "human",
+ "male",
+ "man",
+ "wip"
+ ]
+ },
+ "construction_worker_tone3": {
+ "unicode": "1F477-1F3FD",
+ "unicode_alternates": "",
+ "name": "construction worker tone 3",
+ "shortname": ":construction_worker_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "human",
+ "male",
+ "man",
+ "wip"
+ ]
+ },
+ "construction_worker_tone4": {
+ "unicode": "1F477-1F3FE",
+ "unicode_alternates": "",
+ "name": "construction worker tone 4",
+ "shortname": ":construction_worker_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "human",
+ "male",
+ "man",
+ "wip"
+ ]
+ },
+ "construction_worker_tone5": {
+ "unicode": "1F477-1F3FF",
+ "unicode_alternates": "",
+ "name": "construction worker tone 5",
+ "shortname": ":construction_worker_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "human",
+ "male",
+ "man",
+ "wip"
+ ]
+ },
"control_knobs": {
"unicode": "1F39B",
"unicode_alternates": [],
@@ -2517,7 +5175,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dial"]
+ "keywords": [
+ "dial"
+ ]
},
"contruction_site": {
"unicode": "1F3D7",
@@ -2525,9 +5185,14 @@
"name": "building construction",
"shortname": ":contruction_site:",
"category": "travel_places",
- "aliases": [":building_construction:"],
+ "aliases": [
+ ":building_construction:"
+ ],
"aliases_ascii": [],
- "keywords": ["site", "work"]
+ "keywords": [
+ "site",
+ "work"
+ ]
},
"convenience_store": {
"unicode": "1F3EA",
@@ -2537,7 +5202,9 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building"],
+ "keywords": [
+ "building"
+ ],
"moji": "🏪"
},
"cookie": {
@@ -2548,7 +5215,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chocolate", "food", "oreo", "snack", "cookie", "dessert", "biscuit", "sweet", "chocolate"],
+ "keywords": [
+ "chocolate",
+ "food",
+ "oreo",
+ "snack",
+ "cookie",
+ "dessert",
+ "biscuit",
+ "sweet",
+ "chocolate"
+ ],
"moji": "🍪"
},
"cool": {
@@ -2559,7 +5236,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "words"],
+ "keywords": [
+ "blue-square",
+ "words"
+ ],
"moji": "🆒"
},
"cop": {
@@ -2570,9 +5250,95 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrest", "enforcement", "law", "man", "police"],
+ "keywords": [
+ "arrest",
+ "enforcement",
+ "law",
+ "man",
+ "police"
+ ],
"moji": "👮"
},
+ "cop_tone1": {
+ "unicode": "1F46E-1F3FB",
+ "unicode_alternates": "",
+ "name": "police officer tone 1",
+ "shortname": ":cop_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arrest",
+ "enforcement",
+ "law",
+ "man",
+ "cop"
+ ]
+ },
+ "cop_tone2": {
+ "unicode": "1F46E-1F3FC",
+ "unicode_alternates": "",
+ "name": "police officer tone 2",
+ "shortname": ":cop_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arrest",
+ "enforcement",
+ "law",
+ "man",
+ "cop"
+ ]
+ },
+ "cop_tone3": {
+ "unicode": "1F46E-1F3FD",
+ "unicode_alternates": "",
+ "name": "police officer tone 3",
+ "shortname": ":cop_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arrest",
+ "enforcement",
+ "law",
+ "man",
+ "cop"
+ ]
+ },
+ "cop_tone4": {
+ "unicode": "1F46E-1F3FE",
+ "unicode_alternates": "",
+ "name": "police officer tone 4",
+ "shortname": ":cop_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arrest",
+ "enforcement",
+ "law",
+ "man",
+ "cop"
+ ]
+ },
+ "cop_tone5": {
+ "unicode": "1F46E-1F3FF",
+ "unicode_alternates": "",
+ "name": "police officer tone 5",
+ "shortname": ":cop_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arrest",
+ "enforcement",
+ "law",
+ "man",
+ "cop"
+ ]
+ },
"copyright": {
"moji": "©",
"unicode": "00A9",
@@ -2582,7 +5348,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["ip", "license"]
+ "keywords": [
+ "ip",
+ "license"
+ ]
},
"corn": {
"unicode": "1F33D",
@@ -2592,7 +5361,22 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "plant", "vegetable", "corn", "maize", "food", "iowa", "kernel", "popcorn", "husk", "yellow", "stalk", "cob", "ear"],
+ "keywords": [
+ "food",
+ "plant",
+ "vegetable",
+ "corn",
+ "maize",
+ "food",
+ "iowa",
+ "kernel",
+ "popcorn",
+ "husk",
+ "yellow",
+ "stalk",
+ "cob",
+ "ear"
+ ],
"moji": "🌽"
},
"couch": {
@@ -2601,9 +5385,20 @@
"name": "couch and lamp",
"shortname": ":couch:",
"category": "travel_places",
- "aliases": [":couch_and_lamp:"],
- "aliases_ascii": [],
- "keywords": ["lounge", "sectional", "sofa", "loveseat", "leather", "microfiber", "sit", "relax"]
+ "aliases": [
+ ":couch_and_lamp:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "lounge",
+ "sectional",
+ "sofa",
+ "loveseat",
+ "leather",
+ "microfiber",
+ "sit",
+ "relax"
+ ]
},
"couple": {
"unicode": "1F46B",
@@ -2613,18 +5408,40 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "date", "dating", "human", "like", "love", "marriage", "people", "valentines"],
+ "keywords": [
+ "affection",
+ "date",
+ "dating",
+ "human",
+ "like",
+ "love",
+ "marriage",
+ "people",
+ "valentines"
+ ],
"moji": "👫"
},
"couple_mm": {
"unicode": "1F468-2764-1F468",
- "unicode_alternates": ["1F468-200D-2764-FE0F-200D-1F468"],
+ "unicode_alternates": [
+ "1F468-200D-2764-FE0F-200D-1F468"
+ ],
"name": "couple (man,man)",
"shortname": ":couple_mm:",
"category": "people",
- "aliases": [":couple_with_heart_mm:"],
- "aliases_ascii": [],
- "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"]
+ "aliases": [
+ ":couple_with_heart_mm:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "affection",
+ "dating",
+ "human",
+ "like",
+ "love",
+ "marriage",
+ "valentines"
+ ]
},
"couple_with_heart": {
"unicode": "1F491",
@@ -2634,18 +5451,38 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"],
+ "keywords": [
+ "affection",
+ "dating",
+ "human",
+ "like",
+ "love",
+ "marriage",
+ "valentines"
+ ],
"moji": "💑"
},
"couple_ww": {
"unicode": "1F469-2764-1F469",
- "unicode_alternates": ["1F469-200D-2764-FE0F-200D-1F469"],
+ "unicode_alternates": [
+ "1F469-200D-2764-FE0F-200D-1F469"
+ ],
"name": "couple (woman,woman)",
"shortname": ":couple_ww:",
"category": "people",
- "aliases": [":couple_with_heart_ww:"],
- "aliases_ascii": [],
- "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"]
+ "aliases": [
+ ":couple_with_heart_ww:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "affection",
+ "dating",
+ "human",
+ "like",
+ "love",
+ "marriage",
+ "valentines"
+ ]
},
"couplekiss": {
"unicode": "1F48F",
@@ -2655,7 +5492,13 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dating", "like", "love", "marriage", "valentines"],
+ "keywords": [
+ "dating",
+ "like",
+ "love",
+ "marriage",
+ "valentines"
+ ],
"moji": "💏"
},
"cow": {
@@ -2666,7 +5509,11 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "beef", "ox"],
+ "keywords": [
+ "animal",
+ "beef",
+ "ox"
+ ],
"moji": "🐮"
},
"cow2": {
@@ -2677,18 +5524,46 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "beef", "nature", "ox", "cow", "milk", "dairy", "beef", "bessie", "moo"],
+ "keywords": [
+ "animal",
+ "beef",
+ "nature",
+ "ox",
+ "cow",
+ "milk",
+ "dairy",
+ "beef",
+ "bessie",
+ "moo"
+ ],
"moji": "🐄"
},
+ "crab": {
+ "unicode": "1F980",
+ "unicode_alternates": "",
+ "name": "crab",
+ "shortname": ":crab:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"crayon": {
"unicode": "1F58D",
"unicode_alternates": [],
"name": "lower left crayon",
"shortname": ":crayon:",
"category": "objects_symbols",
- "aliases": [":lower_left_crayon:"],
+ "aliases": [
+ ":lower_left_crayon:"
+ ],
"aliases_ascii": [],
- "keywords": ["write", "draw", "color", "wax"]
+ "keywords": [
+ "write",
+ "draw",
+ "color",
+ "wax"
+ ]
},
"credit_card": {
"unicode": "1F4B3",
@@ -2698,7 +5573,23 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bill", "dollar", "money", "pay", "payment", "credit", "card", "loan", "purchase", "shopping", "mastercard", "visa", "american express", "wallet", "signature"],
+ "keywords": [
+ "bill",
+ "dollar",
+ "money",
+ "pay",
+ "payment",
+ "credit",
+ "card",
+ "loan",
+ "purchase",
+ "shopping",
+ "mastercard",
+ "visa",
+ "american express",
+ "wallet",
+ "signature"
+ ],
"moji": "💳"
},
"crescent_moon": {
@@ -2709,9 +5600,30 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["night", "moon", "crescent", "waxing", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "night",
+ "moon",
+ "crescent",
+ "waxing",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌙"
},
+ "cricket": {
+ "unicode": "1F3CF",
+ "unicode_alternates": "",
+ "name": "cricket bat and ball",
+ "shortname": ":cricket:",
+ "category": "activity",
+ "aliases": [
+ ":cricket_bat_ball:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"crocodile": {
"unicode": "1F40A",
"unicode_alternates": [],
@@ -2720,18 +5632,47 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "crocodile", "croc", "alligator", "gator", "cranky"],
+ "keywords": [
+ "animal",
+ "nature",
+ "crocodile",
+ "croc",
+ "alligator",
+ "gator",
+ "cranky"
+ ],
"moji": "🐊"
},
+ "cross": {
+ "unicode": "271D",
+ "unicode_alternates": "",
+ "name": "latin cross",
+ "shortname": ":cross:",
+ "category": "symbols",
+ "aliases": [
+ ":latin_cross:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "religion",
+ "symbol",
+ "christian"
+ ]
+ },
"cross_heavy": {
"unicode": "1F547",
"unicode_alternates": [],
"name": "heavy latin cross",
"shortname": ":cross_heavy:",
"category": "objects_symbols",
- "aliases": [":heavy_latin_cross:"],
+ "aliases": [
+ ":heavy_latin_cross:"
+ ],
"aliases_ascii": [],
- "keywords": ["religion", "symbol"]
+ "keywords": [
+ "religion",
+ "symbol"
+ ]
},
"cross_white": {
"unicode": "1F546",
@@ -2739,9 +5680,14 @@
"name": "white latin cross",
"shortname": ":cross_white:",
"category": "objects_symbols",
- "aliases": [":white_latin_cross:"],
+ "aliases": [
+ ":white_latin_cross:"
+ ],
"aliases_ascii": [],
- "keywords": ["religion", "symbol"]
+ "keywords": [
+ "religion",
+ "symbol"
+ ]
},
"crossbones": {
"unicode": "1F571",
@@ -2749,9 +5695,15 @@
"name": "black skull and crossbones",
"shortname": ":crossbones:",
"category": "objects_symbols",
- "aliases": [":black_skull_and_crossbones:"],
+ "aliases": [
+ ":black_skull_and_crossbones:"
+ ],
"aliases_ascii": [],
- "keywords": ["poison", "danger", "death"]
+ "keywords": [
+ "poison",
+ "danger",
+ "death"
+ ]
},
"crossed_flags": {
"unicode": "1F38C",
@@ -2761,9 +5713,24 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["japan"],
+ "keywords": [
+ "japan"
+ ],
"moji": "🎌"
},
+ "crossed_swords": {
+ "unicode": "2694",
+ "unicode_alternates": "",
+ "name": "crossed swords",
+ "shortname": ":crossed_swords:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "object",
+ "weapon"
+ ]
+ },
"crown": {
"unicode": "1F451",
"unicode_alternates": [],
@@ -2772,7 +5739,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["king", "kod", "leader", "royalty"],
+ "keywords": [
+ "king",
+ "kod",
+ "leader",
+ "royalty"
+ ],
"moji": "👑"
},
"cruise_ship": {
@@ -2781,9 +5753,15 @@
"name": "passenger ship",
"shortname": ":cruise_ship:",
"category": "travel_places",
- "aliases": [":passenger_ship:"],
+ "aliases": [
+ ":passenger_ship:"
+ ],
"aliases_ascii": [],
- "keywords": ["titanic", "transportation", "boat"]
+ "keywords": [
+ "titanic",
+ "transportation",
+ "boat"
+ ]
},
"cry": {
"unicode": "1F622",
@@ -2792,8 +5770,21 @@
"shortname": ":cry:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":'(", ":'-(", ";(", ";-("],
- "keywords": ["face", "sad", "sad", "cry", "tear", "weep", "tears"],
+ "aliases_ascii": [
+ ":'(",
+ ":'-(",
+ ";(",
+ ";-("
+ ],
+ "keywords": [
+ "face",
+ "sad",
+ "sad",
+ "cry",
+ "tear",
+ "weep",
+ "tears"
+ ],
"moji": "😢"
},
"crying_cat_face": {
@@ -2804,7 +5795,22 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cats", "sad", "tears", "weep", "cry", "cat", "sob", "tears", "sad", "melancholy", "morn", "somber", "hurt"],
+ "keywords": [
+ "animal",
+ "cats",
+ "sad",
+ "tears",
+ "weep",
+ "cry",
+ "cat",
+ "sob",
+ "tears",
+ "sad",
+ "melancholy",
+ "morn",
+ "somber",
+ "hurt"
+ ],
"moji": "😿"
},
"crystal_ball": {
@@ -2815,7 +5821,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["disco", "party"],
+ "keywords": [
+ "disco",
+ "party"
+ ],
"moji": "🔮"
},
"cupid": {
@@ -2826,7 +5835,13 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "heart", "like", "love", "valentines"],
+ "keywords": [
+ "affection",
+ "heart",
+ "like",
+ "love",
+ "valentines"
+ ],
"moji": "💘"
},
"curly_loop": {
@@ -2837,7 +5852,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["scribble"],
+ "keywords": [
+ "scribble"
+ ],
"moji": "➰"
},
"currency_exchange": {
@@ -2848,7 +5865,11 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dollar", "money", "travel"],
+ "keywords": [
+ "dollar",
+ "money",
+ "travel"
+ ],
"moji": "💱"
},
"curry": {
@@ -2859,7 +5880,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "hot", "indian", "spicy", "curry", "spice", "flavor", "food", "meal"],
+ "keywords": [
+ "food",
+ "hot",
+ "indian",
+ "spicy",
+ "curry",
+ "spice",
+ "flavor",
+ "food",
+ "meal"
+ ],
"moji": "🍛"
},
"custard": {
@@ -2870,7 +5901,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["desert", "food", "custard", "cream", "rich", "butter", "dessert", "crème", "brûlée", "french"],
+ "keywords": [
+ "desert",
+ "food",
+ "custard",
+ "cream",
+ "rich",
+ "butter",
+ "dessert",
+ "crème",
+ "brûlée",
+ "french"
+ ],
"moji": "🍮"
},
"customs": {
@@ -2881,7 +5923,17 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["border", "passport", "customs", "travel", "foreign", "goods", "check", "authority", "government"],
+ "keywords": [
+ "border",
+ "passport",
+ "customs",
+ "travel",
+ "foreign",
+ "goods",
+ "check",
+ "authority",
+ "government"
+ ],
"moji": "🛃"
},
"cyclone": {
@@ -2893,7 +5945,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue", "cloud", "swirl", "weather", "cyclone", "hurricane", "typhoon", "storm", "ocean"]
+ "keywords": [
+ "blue",
+ "cloud",
+ "swirl",
+ "weather",
+ "cyclone",
+ "hurricane",
+ "typhoon",
+ "storm",
+ "ocean"
+ ]
},
"dagger": {
"unicode": "1F5E1",
@@ -2901,9 +5963,14 @@
"name": "dagger knife",
"shortname": ":dagger:",
"category": "objects_symbols",
- "aliases": [":dagger_knife:"],
+ "aliases": [
+ ":dagger_knife:"
+ ],
"aliases_ascii": [],
- "keywords": ["blade", "knife"]
+ "keywords": [
+ "blade",
+ "knife"
+ ]
},
"dancer": {
"unicode": "1F483",
@@ -2913,9 +5980,145 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "fun", "girl", "woman", "dance", "dancer", "dress", "fancy", "boogy", "party", "celebrate", "ballet", "tango", "cha cha", "music"],
+ "keywords": [
+ "female",
+ "fun",
+ "girl",
+ "woman",
+ "dance",
+ "dancer",
+ "dress",
+ "fancy",
+ "boogy",
+ "party",
+ "celebrate",
+ "ballet",
+ "tango",
+ "cha cha",
+ "music"
+ ],
"moji": "💃"
},
+ "dancer_tone1": {
+ "unicode": "1F483-1F3FB",
+ "unicode_alternates": "",
+ "name": "dancer tone 1",
+ "shortname": ":dancer_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "fun",
+ "girl",
+ "woman",
+ "dress",
+ "fancy",
+ "boogy",
+ "party",
+ "celebrate",
+ "ballet",
+ "tango",
+ "cha cha",
+ "music"
+ ]
+ },
+ "dancer_tone2": {
+ "unicode": "1F483-1F3FC",
+ "unicode_alternates": "",
+ "name": "dancer tone 2",
+ "shortname": ":dancer_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "fun",
+ "girl",
+ "woman",
+ "dress",
+ "fancy",
+ "boogy",
+ "party",
+ "celebrate",
+ "ballet",
+ "tango",
+ "cha cha",
+ "music"
+ ]
+ },
+ "dancer_tone3": {
+ "unicode": "1F483-1F3FD",
+ "unicode_alternates": "",
+ "name": "dancer tone 3",
+ "shortname": ":dancer_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "fun",
+ "girl",
+ "woman",
+ "dress",
+ "fancy",
+ "boogy",
+ "party",
+ "celebrate",
+ "ballet",
+ "tango",
+ "cha cha",
+ "music"
+ ]
+ },
+ "dancer_tone4": {
+ "unicode": "1F483-1F3FE",
+ "unicode_alternates": "",
+ "name": "dancer tone 4",
+ "shortname": ":dancer_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "fun",
+ "girl",
+ "woman",
+ "dress",
+ "fancy",
+ "boogy",
+ "party",
+ "celebrate",
+ "ballet",
+ "tango",
+ "cha cha",
+ "music"
+ ]
+ },
+ "dancer_tone5": {
+ "unicode": "1F483-1F3FF",
+ "unicode_alternates": "",
+ "name": "dancer tone 5",
+ "shortname": ":dancer_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "fun",
+ "girl",
+ "woman",
+ "dress",
+ "fancy",
+ "boogy",
+ "party",
+ "celebrate",
+ "ballet",
+ "tango",
+ "cha cha",
+ "music"
+ ]
+ },
"dancers": {
"unicode": "1F46F",
"unicode_alternates": [],
@@ -2924,7 +6127,19 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bunny", "female", "girls", "women", "dancing", "dancers", "showgirl", "playboy", "costume", "bunny", "cancan"],
+ "keywords": [
+ "bunny",
+ "female",
+ "girls",
+ "women",
+ "dancing",
+ "dancers",
+ "showgirl",
+ "playboy",
+ "costume",
+ "bunny",
+ "cancan"
+ ],
"moji": "👯"
},
"dango": {
@@ -2935,7 +6150,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "dango", "japanese", "dumpling", "mochi", "balls", "skewer"],
+ "keywords": [
+ "food",
+ "dango",
+ "japanese",
+ "dumpling",
+ "mochi",
+ "balls",
+ "skewer"
+ ],
"moji": "🍡"
},
"dark_sunglasses": {
@@ -2946,7 +6169,10 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shades", "eyes"]
+ "keywords": [
+ "shades",
+ "eyes"
+ ]
},
"dart": {
"unicode": "1F3AF",
@@ -2956,7 +6182,19 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bar", "game", "direct", "hit", "bullseye", "dart", "archery", "game", "fletching", "arrow", "sport"],
+ "keywords": [
+ "bar",
+ "game",
+ "direct",
+ "hit",
+ "bullseye",
+ "dart",
+ "archery",
+ "game",
+ "fletching",
+ "arrow",
+ "sport"
+ ],
"moji": "🎯"
},
"dash": {
@@ -2967,7 +6205,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["air", "fast", "shoo", "wind"],
+ "keywords": [
+ "air",
+ "fast",
+ "shoo",
+ "wind"
+ ],
"moji": "💨"
},
"date": {
@@ -2978,7 +6221,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["calendar", "schedule"],
+ "keywords": [
+ "calendar",
+ "schedule"
+ ],
"moji": "📅"
},
"deciduous_tree": {
@@ -2989,7 +6235,15 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "plant", "deciduous", "tree", "leaves", "fall", "color"],
+ "keywords": [
+ "nature",
+ "plant",
+ "deciduous",
+ "tree",
+ "leaves",
+ "fall",
+ "color"
+ ],
"moji": "🌳"
},
"department_store": {
@@ -3000,7 +6254,16 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "mall", "shopping", "department", "store", "retail", "sale", "merchandise"],
+ "keywords": [
+ "building",
+ "mall",
+ "shopping",
+ "department",
+ "store",
+ "retail",
+ "sale",
+ "merchandise"
+ ],
"moji": "🏬"
},
"descending_notes": {
@@ -3011,7 +6274,12 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["score", "music", "sound", "tone"]
+ "keywords": [
+ "score",
+ "music",
+ "sound",
+ "tone"
+ ]
},
"desert": {
"unicode": "1F3DC",
@@ -3021,7 +6289,14 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["hot", "dry", "sandy", "cactus", "sunny", "barren"]
+ "keywords": [
+ "hot",
+ "dry",
+ "sandy",
+ "cactus",
+ "sunny",
+ "barren"
+ ]
},
"desktop": {
"unicode": "1F5A5",
@@ -3029,9 +6304,13 @@
"name": "desktop computer",
"shortname": ":desktop:",
"category": "objects_symbols",
- "aliases": [":desktop_computer:"],
+ "aliases": [
+ ":desktop_computer:"
+ ],
"aliases_ascii": [],
- "keywords": ["cpu"]
+ "keywords": [
+ "cpu"
+ ]
},
"desktop_window": {
"unicode": "1F5D4",
@@ -3041,7 +6320,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["computer"]
+ "keywords": [
+ "computer"
+ ]
},
"diamond_shape_with_a_dot_inside": {
"unicode": "1F4A0",
@@ -3051,18 +6332,31 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["diamond", "cute", "cuteness", "kawaii", "japanese", "glyph", "adorable"],
+ "keywords": [
+ "diamond",
+ "cute",
+ "cuteness",
+ "kawaii",
+ "japanese",
+ "glyph",
+ "adorable"
+ ],
"moji": "💠"
},
"diamonds": {
"unicode": "2666",
- "unicode_alternates": ["2666-FE0F"],
+ "unicode_alternates": [
+ "2666-FE0F"
+ ],
"name": "black diamond suit",
"shortname": ":diamonds:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cards", "poker"],
+ "keywords": [
+ "cards",
+ "poker"
+ ],
"moji": "♦"
},
"disappointed": {
@@ -3072,8 +6366,24 @@
"shortname": ":disappointed:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [">:[", ":-(", ":(", ":-[", ":[", "=("],
- "keywords": ["disappointed", "disappoint", "frown", "depressed", "discouraged", "face", "sad", "upset"],
+ "aliases_ascii": [
+ ">:[",
+ ":-(",
+ ":(",
+ ":-[",
+ ":[",
+ "=("
+ ],
+ "keywords": [
+ "disappointed",
+ "disappoint",
+ "frown",
+ "depressed",
+ "discouraged",
+ "face",
+ "sad",
+ "upset"
+ ],
"moji": "😞"
},
"disappointed_relieved": {
@@ -3084,7 +6394,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "nervous", "phew", "sweat", "disappoint", "relief"],
+ "keywords": [
+ "face",
+ "nervous",
+ "phew",
+ "sweat",
+ "disappoint",
+ "relief"
+ ],
"moji": "😥"
},
"dividers": {
@@ -3093,9 +6410,14 @@
"name": "card index dividers",
"shortname": ":dividers:",
"category": "objects_symbols",
- "aliases": [":card_index_dividers:"],
+ "aliases": [
+ ":card_index_dividers:"
+ ],
"aliases_ascii": [],
- "keywords": ["stationery", "rolodex"]
+ "keywords": [
+ "stationery",
+ "rolodex"
+ ]
},
"dizzy": {
"unicode": "1F4AB",
@@ -3105,7 +6427,18 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shoot", "sparkle", "star", "dizzy", "drunk", "sick", "intoxicated", "squeans", "starburst", "star"],
+ "keywords": [
+ "shoot",
+ "sparkle",
+ "star",
+ "dizzy",
+ "drunk",
+ "sick",
+ "intoxicated",
+ "squeans",
+ "starburst",
+ "star"
+ ],
"moji": "💫"
},
"dizzy_face": {
@@ -3115,8 +6448,23 @@
"shortname": ":dizzy_face:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["#-)", "#)", "%-)", "%)", "X)", "X-)"],
- "keywords": ["dizzy", "drunk", "inebriated", "face", "spent", "unconscious", "xox"],
+ "aliases_ascii": [
+ "#-)",
+ "#)",
+ "%-)",
+ "%)",
+ "X)",
+ "X-)"
+ ],
+ "keywords": [
+ "dizzy",
+ "drunk",
+ "inebriated",
+ "face",
+ "spent",
+ "unconscious",
+ "xox"
+ ],
"moji": "😵"
},
"do_not_litter": {
@@ -3127,7 +6475,17 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bin", "garbage", "trash", "litter", "garbage", "waste", "no", "can", "trash"],
+ "keywords": [
+ "bin",
+ "garbage",
+ "trash",
+ "litter",
+ "garbage",
+ "waste",
+ "no",
+ "can",
+ "trash"
+ ],
"moji": "🚯"
},
"document": {
@@ -3138,7 +6496,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["page"]
+ "keywords": [
+ "page"
+ ]
},
"document_text": {
"unicode": "1F5B9",
@@ -3146,9 +6506,13 @@
"name": "document with text",
"shortname": ":document_text:",
"category": "objects_symbols",
- "aliases": [":document_with_text:"],
+ "aliases": [
+ ":document_with_text:"
+ ],
"aliases_ascii": [],
- "keywords": ["page"]
+ "keywords": [
+ "page"
+ ]
},
"dog": {
"unicode": "1F436",
@@ -3158,7 +6522,12 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "friend", "nature", "woof"],
+ "keywords": [
+ "animal",
+ "friend",
+ "nature",
+ "woof"
+ ],
"moji": "🐶"
},
"dog2": {
@@ -3169,7 +6538,20 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "doge", "friend", "nature", "pet", "dog", "puppy", "pet", "friend", "woof", "bark", "fido"],
+ "keywords": [
+ "animal",
+ "doge",
+ "friend",
+ "nature",
+ "pet",
+ "dog",
+ "puppy",
+ "pet",
+ "friend",
+ "woof",
+ "bark",
+ "fido"
+ ],
"moji": "🐕"
},
"dollar": {
@@ -3180,7 +6562,21 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bill", "currency", "money", "dollar", "united states", "canada", "australia", "banknote", "money", "currency", "paper", "cash", "bills"],
+ "keywords": [
+ "bill",
+ "currency",
+ "money",
+ "dollar",
+ "united states",
+ "canada",
+ "australia",
+ "banknote",
+ "money",
+ "currency",
+ "paper",
+ "cash",
+ "bills"
+ ],
"moji": "💵"
},
"dolls": {
@@ -3191,7 +6587,23 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["japanese", "kimono", "toy", "dolls", "japan", "japanese", "day", "girls", "emperor", "empress", "pray", "blessing", "imperial", "family", "royal"],
+ "keywords": [
+ "japanese",
+ "kimono",
+ "toy",
+ "dolls",
+ "japan",
+ "japanese",
+ "day",
+ "girls",
+ "emperor",
+ "empress",
+ "pray",
+ "blessing",
+ "imperial",
+ "family",
+ "royal"
+ ],
"moji": "🎎"
},
"dolphin": {
@@ -3202,7 +6614,15 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "fins", "fish", "flipper", "nature", "ocean", "sea"],
+ "keywords": [
+ "animal",
+ "fins",
+ "fish",
+ "flipper",
+ "nature",
+ "ocean",
+ "sea"
+ ],
"moji": "🐬"
},
"door": {
@@ -3213,7 +6633,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["entry", "exit", "house", "door", "doorway", "entrance", "enter", "exit", "entry"],
+ "keywords": [
+ "entry",
+ "exit",
+ "house",
+ "door",
+ "doorway",
+ "entrance",
+ "enter",
+ "exit",
+ "entry"
+ ],
"moji": "🚪"
},
"doughnut": {
@@ -3224,7 +6654,21 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["desert", "food", "snack", "sweet", "doughnut", "donut", "pastry", "fried", "dessert", "breakfast", "police", "homer", "sweet"],
+ "keywords": [
+ "desert",
+ "food",
+ "snack",
+ "sweet",
+ "doughnut",
+ "donut",
+ "pastry",
+ "fried",
+ "dessert",
+ "breakfast",
+ "police",
+ "homer",
+ "sweet"
+ ],
"moji": "🍩"
},
"dove": {
@@ -3233,9 +6677,14 @@
"name": "dove of peace",
"shortname": ":dove:",
"category": "objects_symbols",
- "aliases": [":dove_of_peace:"],
+ "aliases": [
+ ":dove_of_peace:"
+ ],
"aliases_ascii": [],
- "keywords": ["symbol", "bird"]
+ "keywords": [
+ "symbol",
+ "bird"
+ ]
},
"dragon": {
"unicode": "1F409",
@@ -3245,7 +6694,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "chinese", "green", "myth", "nature", "dragon", "fire", "legendary", "myth"],
+ "keywords": [
+ "animal",
+ "chinese",
+ "green",
+ "myth",
+ "nature",
+ "dragon",
+ "fire",
+ "legendary",
+ "myth"
+ ],
"moji": "🐉"
},
"dragon_face": {
@@ -3256,7 +6715,18 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "chinese", "green", "myth", "nature", "dragon", "head", "fire", "legendary", "myth"],
+ "keywords": [
+ "animal",
+ "chinese",
+ "green",
+ "myth",
+ "nature",
+ "dragon",
+ "head",
+ "fire",
+ "legendary",
+ "myth"
+ ],
"moji": "🐲"
},
"dress": {
@@ -3267,7 +6737,10 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clothes", "fashion"],
+ "keywords": [
+ "clothes",
+ "fashion"
+ ],
"moji": "👗"
},
"dromedary_camel": {
@@ -3278,7 +6751,22 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "desert", "hot", "dromedary", "camel", "hump", "desert", "middle east", "heat", "hot", "water", "hump day", "wednesday", "sex"],
+ "keywords": [
+ "animal",
+ "desert",
+ "hot",
+ "dromedary",
+ "camel",
+ "hump",
+ "desert",
+ "middle east",
+ "heat",
+ "hot",
+ "water",
+ "hump day",
+ "wednesday",
+ "sex"
+ ],
"moji": "🐪"
},
"droplet": {
@@ -3289,7 +6777,23 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["drip", "faucet", "water", "drop", "droplet", "h20", "water", "aqua", "tear", "sweat", "rain", "moisture", "wet", "moist", "spit"],
+ "keywords": [
+ "drip",
+ "faucet",
+ "water",
+ "drop",
+ "droplet",
+ "h20",
+ "water",
+ "aqua",
+ "tear",
+ "sweat",
+ "rain",
+ "moisture",
+ "wet",
+ "moist",
+ "spit"
+ ],
"moji": "💧"
},
"dvd": {
@@ -3300,7 +6804,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cd", "disc", "disk"],
+ "keywords": [
+ "cd",
+ "disc",
+ "disk"
+ ],
"moji": "📀"
},
"e-mail": {
@@ -3309,9 +6817,14 @@
"name": "e-mail symbol",
"shortname": ":e-mail:",
"category": "objects",
- "aliases": [":email:"],
- "aliases_ascii": [],
- "keywords": ["communication", "inbox"],
+ "aliases": [
+ ":email:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "communication",
+ "inbox"
+ ],
"moji": "📧"
},
"ear": {
@@ -3322,7 +6835,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "hear", "listen", "sound"],
+ "keywords": [
+ "face",
+ "hear",
+ "listen",
+ "sound"
+ ],
"moji": "👂"
},
"ear_of_rice": {
@@ -3333,9 +6851,87 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "plant", "ear", "rice", "food", "plant", "seed"],
+ "keywords": [
+ "nature",
+ "plant",
+ "ear",
+ "rice",
+ "food",
+ "plant",
+ "seed"
+ ],
"moji": "🌾"
},
+ "ear_tone1": {
+ "unicode": "1F442-1F3FB",
+ "unicode_alternates": "",
+ "name": "ear tone 1",
+ "shortname": ":ear_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hear",
+ "listen",
+ "sound"
+ ]
+ },
+ "ear_tone2": {
+ "unicode": "1F442-1F3FC",
+ "unicode_alternates": "",
+ "name": "ear tone 2",
+ "shortname": ":ear_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hear",
+ "listen",
+ "sound"
+ ]
+ },
+ "ear_tone3": {
+ "unicode": "1F442-1F3FD",
+ "unicode_alternates": "",
+ "name": "ear tone 3",
+ "shortname": ":ear_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hear",
+ "listen",
+ "sound"
+ ]
+ },
+ "ear_tone4": {
+ "unicode": "1F442-1F3FE",
+ "unicode_alternates": "",
+ "name": "ear tone 4",
+ "shortname": ":ear_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hear",
+ "listen",
+ "sound"
+ ]
+ },
+ "ear_tone5": {
+ "unicode": "1F442-1F3FF",
+ "unicode_alternates": "",
+ "name": "ear tone 5",
+ "shortname": ":ear_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hear",
+ "listen",
+ "sound"
+ ]
+ },
"earth_africa": {
"unicode": "1F30D",
"unicode_alternates": [],
@@ -3344,7 +6940,18 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["globe", "international", "world", "earth", "globe", "space", "planet", "africa", "europe", "home"],
+ "keywords": [
+ "globe",
+ "international",
+ "world",
+ "earth",
+ "globe",
+ "space",
+ "planet",
+ "africa",
+ "europe",
+ "home"
+ ],
"moji": "🌍"
},
"earth_americas": {
@@ -3355,7 +6962,21 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["USA", "globe", "international", "world", "earth", "globe", "space", "planet", "north", "south", "america", "americas", "home"],
+ "keywords": [
+ "USA",
+ "globe",
+ "international",
+ "world",
+ "earth",
+ "globe",
+ "space",
+ "planet",
+ "north",
+ "south",
+ "america",
+ "americas",
+ "home"
+ ],
"moji": "🌎"
},
"earth_asia": {
@@ -3366,7 +6987,19 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["east", "globe", "international", "world", "earth", "globe", "space", "planet", "asia", "australia", "home"],
+ "keywords": [
+ "east",
+ "globe",
+ "international",
+ "world",
+ "earth",
+ "globe",
+ "space",
+ "planet",
+ "asia",
+ "australia",
+ "home"
+ ],
"moji": "🌏"
},
"egg": {
@@ -3377,7 +7010,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["breakfast", "food", "egg", "fry", "pan", "flat", "cook", "frying", "cooking", "utensil"],
+ "keywords": [
+ "breakfast",
+ "food",
+ "egg",
+ "fry",
+ "pan",
+ "flat",
+ "cook",
+ "frying",
+ "cooking",
+ "utensil"
+ ],
"moji": "🍳"
},
"eggplant": {
@@ -3388,23 +7032,41 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["aubergine", "food", "nature", "vegetable", "eggplant", "aubergine", "fruit", "purple", "penis"],
+ "keywords": [
+ "aubergine",
+ "food",
+ "nature",
+ "vegetable",
+ "eggplant",
+ "aubergine",
+ "fruit",
+ "purple",
+ "penis"
+ ],
"moji": "🍆"
},
"eight": {
"moji": "8️⃣",
"unicode": "0038-20E3",
- "unicode_alternates": ["0038-FE0F-20E3"],
+ "unicode_alternates": [
+ "0038-FE0F-20E3"
+ ],
"name": "digit eight",
"shortname": ":eight:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["8", "blue-square", "numbers"]
+ "keywords": [
+ "8",
+ "blue-square",
+ "numbers"
+ ]
},
"eight_pointed_black_star": {
"unicode": "2734",
- "unicode_alternates": ["2734-FE0F"],
+ "unicode_alternates": [
+ "2734-FE0F"
+ ],
"name": "eight pointed black star",
"shortname": ":eight_pointed_black_star:",
"category": "other",
@@ -3415,13 +7077,19 @@
},
"eight_spoked_asterisk": {
"unicode": "2733",
- "unicode_alternates": ["2733-FE0F"],
+ "unicode_alternates": [
+ "2733-FE0F"
+ ],
"name": "eight spoked asterisk",
"shortname": ":eight_spoked_asterisk:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["green-square", "sparkle", "star"],
+ "keywords": [
+ "green-square",
+ "sparkle",
+ "star"
+ ],
"moji": "✳"
},
"electric_plug": {
@@ -3432,7 +7100,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["charger", "power"],
+ "keywords": [
+ "charger",
+ "power"
+ ],
"moji": "🔌"
},
"elephant": {
@@ -3443,7 +7114,12 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "nose", "thailand"],
+ "keywords": [
+ "animal",
+ "nature",
+ "nose",
+ "thailand"
+ ],
"moji": "🐘"
},
"end": {
@@ -3454,18 +7130,28 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "words"],
+ "keywords": [
+ "arrow",
+ "words"
+ ],
"moji": "🔚"
},
"envelope": {
"unicode": "2709",
- "unicode_alternates": ["2709-FE0F"],
+ "unicode_alternates": [
+ "2709-FE0F"
+ ],
"name": "envelope",
"shortname": ":envelope:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "letter", "mail", "postal"],
+ "keywords": [
+ "communication",
+ "letter",
+ "mail",
+ "postal"
+ ],
"moji": "✉"
},
"envelope_back": {
@@ -3474,9 +7160,16 @@
"name": "back of envelope",
"shortname": ":envelope_back:",
"category": "objects_symbols",
- "aliases": [":back_of_envelope:"],
+ "aliases": [
+ ":back_of_envelope:"
+ ],
"aliases_ascii": [],
- "keywords": ["communication", "letter", "mail", "postal"]
+ "keywords": [
+ "communication",
+ "letter",
+ "mail",
+ "postal"
+ ]
},
"envelope_flying": {
"unicode": "1F585",
@@ -3484,9 +7177,16 @@
"name": "flying envelope",
"shortname": ":envelope_flying:",
"category": "objects_symbols",
- "aliases": [":flying_envelope:"],
+ "aliases": [
+ ":flying_envelope:"
+ ],
"aliases_ascii": [],
- "keywords": ["communication", "letter", "mail", "postal"]
+ "keywords": [
+ "communication",
+ "letter",
+ "mail",
+ "postal"
+ ]
},
"envelope_stamped": {
"unicode": "1F583",
@@ -3494,9 +7194,16 @@
"name": "stamped envelope",
"shortname": ":envelope_stamped:",
"category": "objects_symbols",
- "aliases": [":stamped_envelope:"],
+ "aliases": [
+ ":stamped_envelope:"
+ ],
"aliases_ascii": [],
- "keywords": ["communication", "letter", "mail", "postal"]
+ "keywords": [
+ "communication",
+ "letter",
+ "mail",
+ "postal"
+ ]
},
"envelope_stamped_pen": {
"unicode": "1F586",
@@ -3504,9 +7211,16 @@
"name": "pen over stamped envelope",
"shortname": ":envelope_stamped_pen:",
"category": "objects_symbols",
- "aliases": [":pen_over_stamped_envelope:"],
+ "aliases": [
+ ":pen_over_stamped_envelope:"
+ ],
"aliases_ascii": [],
- "keywords": ["communication", "letter", "mail", "postal"]
+ "keywords": [
+ "communication",
+ "letter",
+ "mail",
+ "postal"
+ ]
},
"envelope_with_arrow": {
"unicode": "1F4E9",
@@ -3516,7 +7230,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["email"],
+ "keywords": [
+ "email"
+ ],
"moji": "📩"
},
"euro": {
@@ -3527,7 +7243,19 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["currency", "dollar", "money", "euro", "europe", "banknote", "money", "currency", "paper", "cash", "bills"],
+ "keywords": [
+ "currency",
+ "dollar",
+ "money",
+ "euro",
+ "europe",
+ "banknote",
+ "money",
+ "currency",
+ "paper",
+ "cash",
+ "bills"
+ ],
"moji": "💶"
},
"european_castle": {
@@ -3538,7 +7266,29 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "history", "royalty", "castle", "european", "residence", "royalty", "disneyland", "disney", "fort", "fortified", "moat", "tower", "princess", "prince", "lord", "king", "queen", "fortress", "nobel", "stronghold"],
+ "keywords": [
+ "building",
+ "history",
+ "royalty",
+ "castle",
+ "european",
+ "residence",
+ "royalty",
+ "disneyland",
+ "disney",
+ "fort",
+ "fortified",
+ "moat",
+ "tower",
+ "princess",
+ "prince",
+ "lord",
+ "king",
+ "queen",
+ "fortress",
+ "nobel",
+ "stronghold"
+ ],
"moji": "🏰"
},
"european_post_office": {
@@ -3549,7 +7299,9 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building"],
+ "keywords": [
+ "building"
+ ],
"moji": "🏤"
},
"evergreen_tree": {
@@ -3560,18 +7312,29 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "plant", "evergreen", "tree", "needles", "christmas"],
+ "keywords": [
+ "nature",
+ "plant",
+ "evergreen",
+ "tree",
+ "needles",
+ "christmas"
+ ],
"moji": "🌲"
},
"exclamation": {
"unicode": "2757",
- "unicode_alternates": ["2757-FE0F"],
+ "unicode_alternates": [
+ "2757-FE0F"
+ ],
"name": "heavy exclamation mark symbol",
"shortname": ":exclamation:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["surprise"],
+ "keywords": [
+ "surprise"
+ ],
"moji": "❗"
},
"expressionless": {
@@ -3581,8 +7344,20 @@
"shortname": ":expressionless:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["-_-", "-__-", "-___-"],
- "keywords": ["expressionless", "blank", "void", "vapid", "without expression", "face", "indifferent"],
+ "aliases_ascii": [
+ "-_-",
+ "-__-",
+ "-___-"
+ ],
+ "keywords": [
+ "expressionless",
+ "blank",
+ "void",
+ "vapid",
+ "without expression",
+ "face",
+ "indifferent"
+ ],
"moji": "😑"
},
"eye": {
@@ -3593,7 +7368,21 @@
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["look", "peek", "watch"]
+ "keywords": [
+ "look",
+ "peek",
+ "watch"
+ ]
+ },
+ "eye_in_speech_bubble": {
+ "unicode": "1F441-1F5E8",
+ "unicode_alternates": "1f441-200d-1f5e8",
+ "name": "eye in speech bubble",
+ "shortname": ":eye_in_speech_bubble:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
},
"eyeglasses": {
"unicode": "1F453",
@@ -3603,7 +7392,24 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["accessories", "eyesight", "fashion", "eyeglasses", "spectacles", "eye", "sight", "nearsightedness", "myopia", "farsightedness", "hyperopia", "frames", "vision", "see", "blurry", "contacts"],
+ "keywords": [
+ "accessories",
+ "eyesight",
+ "fashion",
+ "eyeglasses",
+ "spectacles",
+ "eye",
+ "sight",
+ "nearsightedness",
+ "myopia",
+ "farsightedness",
+ "hyperopia",
+ "frames",
+ "vision",
+ "see",
+ "blurry",
+ "contacts"
+ ],
"moji": "👓"
},
"eyes": {
@@ -3614,7 +7420,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["look", "peek", "stalk", "watch"],
+ "keywords": [
+ "look",
+ "peek",
+ "stalk",
+ "watch"
+ ],
"moji": "👀"
},
"factory": {
@@ -3625,7 +7436,9 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building"],
+ "keywords": [
+ "building"
+ ],
"moji": "🏭"
},
"fallen_leaf": {
@@ -3636,7 +7449,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["leaves", "nature", "plant", "vegetable", "leaf", "fall", "color", "deciduous", "autumn"],
+ "keywords": [
+ "leaves",
+ "nature",
+ "plant",
+ "vegetable",
+ "leaf",
+ "fall",
+ "color",
+ "deciduous",
+ "autumn"
+ ],
"moji": "🍂"
},
"family": {
@@ -3647,148 +7470,359 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["child", "dad", "father", "home", "mom", "mother", "parents", "family", "mother", "father", "child", "girl", "boy", "group", "unit"],
+ "keywords": [
+ "child",
+ "dad",
+ "father",
+ "home",
+ "mom",
+ "mother",
+ "parents",
+ "family",
+ "mother",
+ "father",
+ "child",
+ "girl",
+ "boy",
+ "group",
+ "unit"
+ ],
"moji": "👪"
},
"family_mmb": {
"unicode": "1F468-1F468-1F466",
- "unicode_alternates": ["1F468-200D-1F468-200D-1F466"],
+ "unicode_alternates": [
+ "1F468-200D-1F468-200D-1F466"
+ ],
"name": "family (man,man,boy)",
"shortname": ":family_mmb:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["child", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "boy"]
+ "keywords": [
+ "child",
+ "dad",
+ "father",
+ "parents",
+ "group",
+ "unit",
+ "gay",
+ "homosexual",
+ "man",
+ "boy"
+ ]
},
"family_mmbb": {
"unicode": "1F468-1F468-1F466-1F466",
- "unicode_alternates": ["1F468-200D-1F468-200D-1F466-200D-1F466"],
+ "unicode_alternates": [
+ "1F468-200D-1F468-200D-1F466-200D-1F466"
+ ],
"name": "family (man,man,boy,boy)",
"shortname": ":family_mmbb:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "boy"]
+ "keywords": [
+ "children",
+ "dad",
+ "father",
+ "parents",
+ "group",
+ "unit",
+ "gay",
+ "homosexual",
+ "man",
+ "boy"
+ ]
},
"family_mmg": {
"unicode": "1F468-1F468-1F467",
- "unicode_alternates": ["1F468-200D-1F468-200D-1F467"],
+ "unicode_alternates": [
+ "1F468-200D-1F468-200D-1F467"
+ ],
"name": "family (man,man,girl)",
"shortname": ":family_mmg:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["child", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl"]
+ "keywords": [
+ "child",
+ "dad",
+ "father",
+ "parents",
+ "group",
+ "unit",
+ "gay",
+ "homosexual",
+ "man",
+ "girl"
+ ]
},
"family_mmgb": {
"unicode": "1F468-1F468-1F467-1F466",
- "unicode_alternates": ["1F468-200D-1F468-200D-1F467-200D-1F466"],
+ "unicode_alternates": [
+ "1F468-200D-1F468-200D-1F467-200D-1F466"
+ ],
"name": "family (man,man,girl,boy)",
"shortname": ":family_mmgb:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl", "boy"]
+ "keywords": [
+ "children",
+ "dad",
+ "father",
+ "parents",
+ "group",
+ "unit",
+ "gay",
+ "homosexual",
+ "man",
+ "girl",
+ "boy"
+ ]
},
"family_mmgg": {
"unicode": "1F468-1F468-1F467-1F467",
- "unicode_alternates": ["1F468-200D-1F468-200D-1F467-200D-1F467"],
+ "unicode_alternates": [
+ "1F468-200D-1F468-200D-1F467-200D-1F467"
+ ],
"name": "family (man,man,girl,girl)",
"shortname": ":family_mmgg:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl"]
+ "keywords": [
+ "children",
+ "dad",
+ "father",
+ "parents",
+ "group",
+ "unit",
+ "gay",
+ "homosexual",
+ "man",
+ "girl"
+ ]
},
"family_mwbb": {
"unicode": "1F468-1F469-1F466-1F466",
- "unicode_alternates": ["1F468-200D-1F469-200D-1F466-200D-1F466"],
+ "unicode_alternates": [
+ "1F468-200D-1F469-200D-1F466-200D-1F466"
+ ],
"name": "family (man,woman,boy,boy)",
"shortname": ":family_mwbb:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dad", "father", "mom", "mother", "parents", "children", "boy", "group", "unit", "man", "woman"]
+ "keywords": [
+ "dad",
+ "father",
+ "mom",
+ "mother",
+ "parents",
+ "children",
+ "boy",
+ "group",
+ "unit",
+ "man",
+ "woman"
+ ]
},
"family_mwg": {
"unicode": "1F468-1F469-1F467",
- "unicode_alternates": ["1F468-200D-1F469-200D-1F467"],
+ "unicode_alternates": [
+ "1F468-200D-1F469-200D-1F467"
+ ],
"name": "family (man,woman,girl)",
"shortname": ":family_mwg:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["child", "dad", "father", "mom", "mother", "parents", "girl", "boy", "group", "unit", "man", "woman"]
+ "keywords": [
+ "child",
+ "dad",
+ "father",
+ "mom",
+ "mother",
+ "parents",
+ "girl",
+ "boy",
+ "group",
+ "unit",
+ "man",
+ "woman"
+ ]
},
"family_mwgb": {
"unicode": "1F468-1F469-1F467-1F466",
- "unicode_alternates": ["1F468-200D-1F469-200D-1F467-200D-1F466"],
+ "unicode_alternates": [
+ "1F468-200D-1F469-200D-1F467-200D-1F466"
+ ],
"name": "family (man,woman,girl,boy)",
"shortname": ":family_mwgb:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dad", "father", "mom", "mother", "parents", "children", "girl", "boy", "group", "unit", "man", "woman"]
+ "keywords": [
+ "dad",
+ "father",
+ "mom",
+ "mother",
+ "parents",
+ "children",
+ "girl",
+ "boy",
+ "group",
+ "unit",
+ "man",
+ "woman"
+ ]
},
"family_mwgg": {
"unicode": "1F468-1F469-1F467-1F467",
- "unicode_alternates": ["1F468-200D-1F469-200D-1F467-200D-1F467"],
+ "unicode_alternates": [
+ "1F468-200D-1F469-200D-1F467-200D-1F467"
+ ],
"name": "family (man,woman,girl,girl)",
"shortname": ":family_mwgg:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dad", "father", "mom", "mother", "parents", "children", "girl", "group", "unit", "man", "woman"]
+ "keywords": [
+ "dad",
+ "father",
+ "mom",
+ "mother",
+ "parents",
+ "children",
+ "girl",
+ "group",
+ "unit",
+ "man",
+ "woman"
+ ]
},
"family_wwb": {
"unicode": "1F469-1F469-1F466",
- "unicode_alternates": ["1F469-200D-1F469-200D-1F466"],
+ "unicode_alternates": [
+ "1F469-200D-1F469-200D-1F466"
+ ],
"name": "family (woman,woman,boy)",
"shortname": ":family_wwb:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mom", "mother", "parents", "child", "boy", "group", "unit", "gay", "lesbian", "homosexual", "woman"]
+ "keywords": [
+ "mom",
+ "mother",
+ "parents",
+ "child",
+ "boy",
+ "group",
+ "unit",
+ "gay",
+ "lesbian",
+ "homosexual",
+ "woman"
+ ]
},
"family_wwbb": {
"unicode": "1F469-1F469-1F466-1F466",
- "unicode_alternates": ["1F469-200D-1F469-200D-1F466-200D-1F466"],
+ "unicode_alternates": [
+ "1F469-200D-1F469-200D-1F466-200D-1F466"
+ ],
"name": "family (woman,woman,boy,boy)",
"shortname": ":family_wwbb:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "boy"]
+ "keywords": [
+ "mom",
+ "mother",
+ "parents",
+ "children",
+ "group",
+ "unit",
+ "gay",
+ "lesbian",
+ "homosexual",
+ "woman",
+ "boy"
+ ]
},
"family_wwg": {
"unicode": "1F469-1F469-1F467",
- "unicode_alternates": ["1F469-200D-1F469-200D-1F467"],
+ "unicode_alternates": [
+ "1F469-200D-1F469-200D-1F467"
+ ],
"name": "family (woman,woman,girl)",
"shortname": ":family_wwg:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mom", "mother", "parents", "child", "woman", "girl", "group", "unit", "gay", "lesbian", "homosexual"]
+ "keywords": [
+ "mom",
+ "mother",
+ "parents",
+ "child",
+ "woman",
+ "girl",
+ "group",
+ "unit",
+ "gay",
+ "lesbian",
+ "homosexual"
+ ]
},
"family_wwgb": {
"unicode": "1F469-1F469-1F467-1F466",
- "unicode_alternates": ["1F469-200D-1F469-200D-1F467-200D-1F466"],
+ "unicode_alternates": [
+ "1F469-200D-1F469-200D-1F467-200D-1F466"
+ ],
"name": "family (woman,woman,girl,boy)",
"shortname": ":family_wwgb:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "girl", "boy"]
+ "keywords": [
+ "mom",
+ "mother",
+ "parents",
+ "children",
+ "group",
+ "unit",
+ "gay",
+ "lesbian",
+ "homosexual",
+ "woman",
+ "girl",
+ "boy"
+ ]
},
"family_wwgg": {
"unicode": "1F469-1F469-1F467-1F467",
- "unicode_alternates": ["1F469-200D-1F469-200D-1F467-200D-1F467"],
+ "unicode_alternates": [
+ "1F469-200D-1F469-200D-1F467-200D-1F467"
+ ],
"name": "family (woman,woman,girl,girl)",
"shortname": ":family_wwgg:",
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "girl"]
+ "keywords": [
+ "mom",
+ "mother",
+ "parents",
+ "children",
+ "group",
+ "unit",
+ "gay",
+ "lesbian",
+ "homosexual",
+ "woman",
+ "girl"
+ ]
},
"fast_forward": {
"unicode": "23E9",
@@ -3798,7 +7832,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "⏩"
},
"fax": {
@@ -3809,7 +7845,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "technology"],
+ "keywords": [
+ "communication",
+ "technology"
+ ],
"moji": "📠"
},
"fearful": {
@@ -3820,7 +7859,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "nervous", "oops", "scared", "terrified", "fear", "fearful", "scared", "frightened"],
+ "keywords": [
+ "face",
+ "nervous",
+ "oops",
+ "scared",
+ "terrified",
+ "fear",
+ "fearful",
+ "scared",
+ "frightened"
+ ],
"moji": "😨"
},
"feet": {
@@ -3831,7 +7880,29 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cat", "dog", "footprints", "paw", "pet", "tracking", "paw", "prints", "mark", "imprints", "footsteps", "animal", "lion", "bear", "dog", "cat", "raccoon", "critter", "feet", "pawsteps"],
+ "keywords": [
+ "animal",
+ "cat",
+ "dog",
+ "footprints",
+ "paw",
+ "pet",
+ "tracking",
+ "paw",
+ "prints",
+ "mark",
+ "imprints",
+ "footsteps",
+ "animal",
+ "lion",
+ "bear",
+ "dog",
+ "cat",
+ "raccoon",
+ "critter",
+ "feet",
+ "pawsteps"
+ ],
"moji": "🐾"
},
"ferris_wheel": {
@@ -3842,9 +7913,44 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["carnival", "londoneye", "photo", "farris", "wheel", "amusement", "park", "fair", "ride", "entertainment"],
+ "keywords": [
+ "carnival",
+ "londoneye",
+ "photo",
+ "farris",
+ "wheel",
+ "amusement",
+ "park",
+ "fair",
+ "ride",
+ "entertainment"
+ ],
"moji": "🎡"
},
+ "ferry": {
+ "unicode": "26F4",
+ "unicode_alternates": "",
+ "name": "ferry",
+ "shortname": ":ferry:",
+ "category": "travel",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boat",
+ "place",
+ "travel"
+ ]
+ },
+ "field_hockey": {
+ "unicode": "1F3D1",
+ "unicode_alternates": "",
+ "name": "field hockey stick and ball",
+ "shortname": ":field_hockey:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"file_cabinet": {
"unicode": "1F5C4",
"unicode_alternates": [],
@@ -3853,7 +7959,12 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["folders", "office", "documents", "storage"]
+ "keywords": [
+ "folders",
+ "office",
+ "documents",
+ "storage"
+ ]
},
"file_folder": {
"unicode": "1F4C1",
@@ -3863,7 +7974,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents"],
+ "keywords": [
+ "documents"
+ ],
"moji": "📁"
},
"film_frames": {
@@ -3874,7 +7987,14 @@
"category": "activity",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["movie", "record", "8mm", "16mm", "reel", "celluloid"]
+ "keywords": [
+ "movie",
+ "record",
+ "8mm",
+ "16mm",
+ "reel",
+ "celluloid"
+ ]
},
"finger_pointing_down": {
"unicode": "1F597",
@@ -3882,9 +8002,15 @@
"name": "white down pointing left hand index",
"shortname": ":finger_pointing_down:",
"category": "people",
- "aliases": [":white_down_pointing_left_hand_index:"],
+ "aliases": [
+ ":white_down_pointing_left_hand_index:"
+ ],
"aliases_ascii": [],
- "keywords": ["direction", "finger", "hand"]
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
},
"finger_pointing_down2": {
"unicode": "1F59F",
@@ -3892,9 +8018,15 @@
"name": "sideways white down pointing index",
"shortname": ":finger_pointing_down2:",
"category": "people",
- "aliases": [":sideways_white_down_pointing_index:"],
+ "aliases": [
+ ":sideways_white_down_pointing_index:"
+ ],
"aliases_ascii": [],
- "keywords": ["direction", "finger", "hand"]
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
},
"finger_pointing_left": {
"unicode": "1F598",
@@ -3902,9 +8034,15 @@
"name": "sideways white left pointing index",
"shortname": ":finger_pointing_left:",
"category": "people",
- "aliases": [":sideways_white_left_pointing_index:"],
+ "aliases": [
+ ":sideways_white_left_pointing_index:"
+ ],
"aliases_ascii": [],
- "keywords": ["direction", "finger", "hand"]
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
},
"finger_pointing_right": {
"unicode": "1F599",
@@ -3912,9 +8050,15 @@
"name": "sideways white right pointing index",
"shortname": ":finger_pointing_right:",
"category": "people",
- "aliases": [":sideways_white_right_pointing_index:"],
+ "aliases": [
+ ":sideways_white_right_pointing_index:"
+ ],
"aliases_ascii": [],
- "keywords": ["direction", "finger", "hand"]
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
},
"finger_pointing_up": {
"unicode": "1F59E",
@@ -3922,9 +8066,15 @@
"name": "sideways white up pointing index",
"shortname": ":finger_pointing_up:",
"category": "people",
- "aliases": [":sideways_white_up_pointing_index:"],
+ "aliases": [
+ ":sideways_white_up_pointing_index:"
+ ],
"aliases_ascii": [],
- "keywords": ["direction", "finger", "hand"]
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
},
"fire": {
"unicode": "1F525",
@@ -3932,9 +8082,15 @@
"name": "fire",
"shortname": ":fire:",
"category": "emoticons",
- "aliases": [":flame:"],
- "aliases_ascii": [],
- "keywords": ["cook", "hot", "flame"],
+ "aliases": [
+ ":flame:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cook",
+ "hot",
+ "flame"
+ ],
"moji": "🔥"
},
"fire_engine": {
@@ -3945,7 +8101,17 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cars", "transportation", "vehicle", "fire", "fighter", "engine", "truck", "emergency", "medical"],
+ "keywords": [
+ "cars",
+ "transportation",
+ "vehicle",
+ "fire",
+ "fighter",
+ "engine",
+ "truck",
+ "emergency",
+ "medical"
+ ],
"moji": "🚒"
},
"fire_engine_oncoming": {
@@ -3954,9 +8120,17 @@
"name": "oncoming fire engine",
"shortname": ":fire_engine_oncoming:",
"category": "travel_places",
- "aliases": [":oncoming_fire_engine:"],
- "aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "fighter", "truck", "emergency"]
+ "aliases": [
+ ":oncoming_fire_engine:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "fighter",
+ "truck",
+ "emergency"
+ ]
},
"fireworks": {
"unicode": "1F386",
@@ -3966,7 +8140,22 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["carnival", "congratulations", "festival", "photo", "fireworks", "independence", "celebration", "explosion", "july", "4th", "rocket", "sky", "idea", "excitement"],
+ "keywords": [
+ "carnival",
+ "congratulations",
+ "festival",
+ "photo",
+ "fireworks",
+ "independence",
+ "celebration",
+ "explosion",
+ "july",
+ "4th",
+ "rocket",
+ "sky",
+ "idea",
+ "excitement"
+ ],
"moji": "🎆"
},
"first_quarter_moon": {
@@ -3977,7 +8166,16 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "quarter", "first", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "quarter",
+ "first",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌓"
},
"first_quarter_moon_with_face": {
@@ -3988,7 +8186,18 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "first", "quarter", "anthropomorphic", "face", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "first",
+ "quarter",
+ "anthropomorphic",
+ "face",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌛"
},
"fish": {
@@ -3999,7 +8208,11 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "food", "nature"],
+ "keywords": [
+ "animal",
+ "food",
+ "nature"
+ ],
"moji": "🐟"
},
"fish_cake": {
@@ -4010,7 +8223,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fish", "cake", "kamboko", "swirl", "ramen", "noodles", "naruto"],
+ "keywords": [
+ "food",
+ "fish",
+ "cake",
+ "kamboko",
+ "swirl",
+ "ramen",
+ "noodles",
+ "naruto"
+ ],
"moji": "🍥"
},
"fishing_pole_and_fish": {
@@ -4021,7 +8243,13 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "hobby", "fish", "fishing", "pole"],
+ "keywords": [
+ "food",
+ "hobby",
+ "fish",
+ "fishing",
+ "pole"
+ ],
"moji": "🎣"
},
"fist": {
@@ -4032,19 +8260,99 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fingers", "grasp", "hand"],
+ "keywords": [
+ "fingers",
+ "grasp",
+ "hand"
+ ],
"moji": "✊"
},
+ "fist_tone1": {
+ "unicode": "270A-1F3FB",
+ "unicode_alternates": "",
+ "name": "raised fist tone 1",
+ "shortname": ":fist_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "grasp",
+ "hand"
+ ]
+ },
+ "fist_tone2": {
+ "unicode": "270A-1F3FC",
+ "unicode_alternates": "",
+ "name": "raised fist tone 2",
+ "shortname": ":fist_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "grasp",
+ "hand"
+ ]
+ },
+ "fist_tone3": {
+ "unicode": "270A-1F3FD",
+ "unicode_alternates": "",
+ "name": "raised fist tone 3",
+ "shortname": ":fist_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "grasp",
+ "hand"
+ ]
+ },
+ "fist_tone4": {
+ "unicode": "270A-1F3FE",
+ "unicode_alternates": "",
+ "name": "raised fist tone 4",
+ "shortname": ":fist_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "grasp",
+ "hand"
+ ]
+ },
+ "fist_tone5": {
+ "unicode": "270A-1F3FF",
+ "unicode_alternates": "",
+ "name": "raised fist tone 5",
+ "shortname": ":fist_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "grasp",
+ "hand"
+ ]
+ },
"five": {
"moji": "5️⃣",
"unicode": "0035-20E3",
- "unicode_alternates": ["0035-FE0F-20E3"],
+ "unicode_alternates": [
+ "0035-FE0F-20E3"
+ ],
"name": "digit five",
"shortname": ":five:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "numbers", "prime"]
+ "keywords": [
+ "blue-square",
+ "numbers",
+ "prime"
+ ]
},
"flag_ac": {
"unicode": "1F1E6-1F1E8",
@@ -4052,9 +8360,15 @@
"name": "ascension",
"shortname": ":flag_ac:",
"category": "flags",
- "aliases": [":ac:"],
+ "aliases": [
+ ":ac:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ac"]
+ "keywords": [
+ "country",
+ "nation",
+ "ac"
+ ]
},
"flag_ad": {
"unicode": "1F1E6-1F1E9",
@@ -4062,9 +8376,15 @@
"name": "andorra",
"shortname": ":flag_ad:",
"category": "flags",
- "aliases": [":ad:"],
+ "aliases": [
+ ":ad:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ad"]
+ "keywords": [
+ "country",
+ "nation",
+ "ad"
+ ]
},
"flag_ae": {
"unicode": "1F1E6-1F1EA",
@@ -4072,9 +8392,15 @@
"name": "the united arab emirates",
"shortname": ":flag_ae:",
"category": "flags",
- "aliases": [":ae:"],
+ "aliases": [
+ ":ae:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ae"]
+ "keywords": [
+ "country",
+ "nation",
+ "ae"
+ ]
},
"flag_af": {
"unicode": "1F1E6-1F1EB",
@@ -4082,9 +8408,16 @@
"name": "afghanistan",
"shortname": ":flag_af:",
"category": "flags",
- "aliases": [":af:"],
+ "aliases": [
+ ":af:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "afghanestan", "af"]
+ "keywords": [
+ "country",
+ "nation",
+ "afghanestan",
+ "af"
+ ]
},
"flag_ag": {
"unicode": "1F1E6-1F1EC",
@@ -4092,9 +8425,15 @@
"name": "antigua and barbuda",
"shortname": ":flag_ag:",
"category": "flags",
- "aliases": [":ag:"],
+ "aliases": [
+ ":ag:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ag"]
+ "keywords": [
+ "country",
+ "nation",
+ "ag"
+ ]
},
"flag_ai": {
"unicode": "1F1E6-1F1EE",
@@ -4102,9 +8441,15 @@
"name": "anguilla",
"shortname": ":flag_ai:",
"category": "flags",
- "aliases": [":ai:"],
+ "aliases": [
+ ":ai:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ai"]
+ "keywords": [
+ "country",
+ "nation",
+ "ai"
+ ]
},
"flag_al": {
"unicode": "1F1E6-1F1F1",
@@ -4112,9 +8457,16 @@
"name": "albania",
"shortname": ":flag_al:",
"category": "flags",
- "aliases": [":al:"],
+ "aliases": [
+ ":al:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "shqiperia", "al"]
+ "keywords": [
+ "country",
+ "nation",
+ "shqiperia",
+ "al"
+ ]
},
"flag_am": {
"unicode": "1F1E6-1F1F2",
@@ -4122,9 +8474,16 @@
"name": "armenia",
"shortname": ":flag_am:",
"category": "flags",
- "aliases": [":am:"],
+ "aliases": [
+ ":am:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "hayastan", "am"]
+ "keywords": [
+ "country",
+ "nation",
+ "hayastan",
+ "am"
+ ]
},
"flag_ao": {
"unicode": "1F1E6-1F1F4",
@@ -4132,9 +8491,27 @@
"name": "angola",
"shortname": ":flag_ao:",
"category": "flags",
- "aliases": [":ao:"],
+ "aliases": [
+ ":ao:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ao"
+ ]
+ },
+ "flag_aq": {
+ "unicode": "1F1E6-1F1F6",
+ "unicode_alternates": "",
+ "name": "antarctica",
+ "shortname": ":flag_aq:",
+ "category": "flags",
+ "aliases": [
+ ":aq:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ao"]
+ "keywords": []
},
"flag_ar": {
"unicode": "1F1E6-1F1F7",
@@ -4142,9 +8519,27 @@
"name": "argentina",
"shortname": ":flag_ar:",
"category": "flags",
- "aliases": [":ar:"],
+ "aliases": [
+ ":ar:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ar"
+ ]
+ },
+ "flag_as": {
+ "unicode": "1F1E6-1F1F8",
+ "unicode_alternates": "",
+ "name": "american samoa",
+ "shortname": ":flag_as:",
+ "category": "flags",
+ "aliases": [
+ ":as:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ar"]
+ "keywords": []
},
"flag_at": {
"unicode": "1F1E6-1F1F9",
@@ -4152,9 +8547,17 @@
"name": "austria",
"shortname": ":flag_at:",
"category": "flags",
- "aliases": [":at:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "&ouml;sterreich", "osterreich", "at"]
+ "aliases": [
+ ":at:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "&ouml;sterreich",
+ "osterreich",
+ "at"
+ ]
},
"flag_au": {
"unicode": "1F1E6-1F1FA",
@@ -4162,9 +8565,15 @@
"name": "australia",
"shortname": ":flag_au:",
"category": "flags",
- "aliases": [":au:"],
+ "aliases": [
+ ":au:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "au"]
+ "keywords": [
+ "country",
+ "nation",
+ "au"
+ ]
},
"flag_aw": {
"unicode": "1F1E6-1F1FC",
@@ -4172,9 +8581,27 @@
"name": "aruba",
"shortname": ":flag_aw:",
"category": "flags",
- "aliases": [":aw:"],
+ "aliases": [
+ ":aw:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "aw"
+ ]
+ },
+ "flag_ax": {
+ "unicode": "1F1E6-1F1FD",
+ "unicode_alternates": "",
+ "name": "åland islands",
+ "shortname": ":flag_ax:",
+ "category": "flags",
+ "aliases": [
+ ":ax:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "aw"]
+ "keywords": []
},
"flag_az": {
"unicode": "1F1E6-1F1FF",
@@ -4182,9 +8609,16 @@
"name": "azerbaijan",
"shortname": ":flag_az:",
"category": "flags",
- "aliases": [":az:"],
+ "aliases": [
+ ":az:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "azarbaycan", "az"]
+ "keywords": [
+ "country",
+ "nation",
+ "azarbaycan",
+ "az"
+ ]
},
"flag_ba": {
"unicode": "1F1E7-1F1E6",
@@ -4192,9 +8626,16 @@
"name": "bosnia and herzegovina",
"shortname": ":flag_ba:",
"category": "flags",
- "aliases": [":ba:"],
+ "aliases": [
+ ":ba:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bosna i hercegovina", "ba"]
+ "keywords": [
+ "country",
+ "nation",
+ "bosna i hercegovina",
+ "ba"
+ ]
},
"flag_bb": {
"unicode": "1F1E7-1F1E7",
@@ -4202,9 +8643,15 @@
"name": "barbados",
"shortname": ":flag_bb:",
"category": "flags",
- "aliases": [":bb:"],
+ "aliases": [
+ ":bb:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bb"]
+ "keywords": [
+ "country",
+ "nation",
+ "bb"
+ ]
},
"flag_bd": {
"unicode": "1F1E7-1F1E9",
@@ -4212,9 +8659,15 @@
"name": "bangladesh",
"shortname": ":flag_bd:",
"category": "flags",
- "aliases": [":bd:"],
+ "aliases": [
+ ":bd:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bd"]
+ "keywords": [
+ "country",
+ "nation",
+ "bd"
+ ]
},
"flag_be": {
"unicode": "1F1E7-1F1EA",
@@ -4222,9 +8675,17 @@
"name": "belgium",
"shortname": ":flag_be:",
"category": "flags",
- "aliases": [":be:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "belgique", "belgie", "be"]
+ "aliases": [
+ ":be:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "belgique",
+ "belgie",
+ "be"
+ ]
},
"flag_bf": {
"unicode": "1F1E7-1F1EB",
@@ -4232,9 +8693,15 @@
"name": "burkina faso",
"shortname": ":flag_bf:",
"category": "flags",
- "aliases": [":bf:"],
+ "aliases": [
+ ":bf:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bf"]
+ "keywords": [
+ "country",
+ "nation",
+ "bf"
+ ]
},
"flag_bg": {
"unicode": "1F1E7-1F1EC",
@@ -4242,9 +8709,15 @@
"name": "bulgaria",
"shortname": ":flag_bg:",
"category": "flags",
- "aliases": [":bg:"],
+ "aliases": [
+ ":bg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bg"]
+ "keywords": [
+ "country",
+ "nation",
+ "bg"
+ ]
},
"flag_bh": {
"unicode": "1F1E7-1F1ED",
@@ -4252,9 +8725,16 @@
"name": "bahrain",
"shortname": ":flag_bh:",
"category": "flags",
- "aliases": [":bh:"],
+ "aliases": [
+ ":bh:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "al bahrayn", "bh"]
+ "keywords": [
+ "country",
+ "nation",
+ "al bahrayn",
+ "bh"
+ ]
},
"flag_bi": {
"unicode": "1F1E7-1F1EE",
@@ -4262,9 +8742,15 @@
"name": "burundi",
"shortname": ":flag_bi:",
"category": "flags",
- "aliases": [":bi:"],
+ "aliases": [
+ ":bi:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bi"]
+ "keywords": [
+ "country",
+ "nation",
+ "bi"
+ ]
},
"flag_bj": {
"unicode": "1F1E7-1F1EF",
@@ -4272,9 +8758,27 @@
"name": "benin",
"shortname": ":flag_bj:",
"category": "flags",
- "aliases": [":bj:"],
+ "aliases": [
+ ":bj:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "bj"
+ ]
+ },
+ "flag_bl": {
+ "unicode": "1F1E7-1F1F1",
+ "unicode_alternates": "",
+ "name": "saint barthélemy",
+ "shortname": ":flag_bl:",
+ "category": "flags",
+ "aliases": [
+ ":bl:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bj"]
+ "keywords": []
},
"flag_black": {
"unicode": "1F3F4",
@@ -4282,9 +8786,14 @@
"name": "waving black flag",
"shortname": ":flag_black:",
"category": "objects_symbols",
- "aliases": [":waving_black_flag:"],
+ "aliases": [
+ ":waving_black_flag:"
+ ],
"aliases_ascii": [],
- "keywords": ["symbol", "signal"]
+ "keywords": [
+ "symbol",
+ "signal"
+ ]
},
"flag_bm": {
"unicode": "1F1E7-1F1F2",
@@ -4292,9 +8801,15 @@
"name": "bermuda",
"shortname": ":flag_bm:",
"category": "flags",
- "aliases": [":bm:"],
+ "aliases": [
+ ":bm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bm"]
+ "keywords": [
+ "country",
+ "nation",
+ "bm"
+ ]
},
"flag_bn": {
"unicode": "1F1E7-1F1F3",
@@ -4302,9 +8817,15 @@
"name": "brunei",
"shortname": ":flag_bn:",
"category": "flags",
- "aliases": [":bn:"],
+ "aliases": [
+ ":bn:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bn"]
+ "keywords": [
+ "country",
+ "nation",
+ "bn"
+ ]
},
"flag_bo": {
"unicode": "1F1E7-1F1F4",
@@ -4312,9 +8833,27 @@
"name": "bolivia",
"shortname": ":flag_bo:",
"category": "flags",
- "aliases": [":bo:"],
+ "aliases": [
+ ":bo:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "bo"
+ ]
+ },
+ "flag_bq": {
+ "unicode": "1F1E7-1F1F6",
+ "unicode_alternates": "",
+ "name": "caribbean netherlands",
+ "shortname": ":flag_bq:",
+ "category": "flags",
+ "aliases": [
+ ":bq:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bo"]
+ "keywords": []
},
"flag_br": {
"unicode": "1F1E7-1F1F7",
@@ -4322,9 +8861,16 @@
"name": "brazil",
"shortname": ":flag_br:",
"category": "flags",
- "aliases": [":br:"],
+ "aliases": [
+ ":br:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "brasil", "br"]
+ "keywords": [
+ "country",
+ "nation",
+ "brasil",
+ "br"
+ ]
},
"flag_bs": {
"unicode": "1F1E7-1F1F8",
@@ -4332,9 +8878,15 @@
"name": "the bahamas",
"shortname": ":flag_bs:",
"category": "flags",
- "aliases": [":bs:"],
+ "aliases": [
+ ":bs:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bs"]
+ "keywords": [
+ "country",
+ "nation",
+ "bs"
+ ]
},
"flag_bt": {
"unicode": "1F1E7-1F1F9",
@@ -4342,9 +8894,27 @@
"name": "bhutan",
"shortname": ":flag_bt:",
"category": "flags",
- "aliases": [":bt:"],
+ "aliases": [
+ ":bt:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "bt"
+ ]
+ },
+ "flag_bv": {
+ "unicode": "1F1E7-1F1FB",
+ "unicode_alternates": "",
+ "name": "bouvet island",
+ "shortname": ":flag_bv:",
+ "category": "flags",
+ "aliases": [
+ ":bv:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bt"]
+ "keywords": []
},
"flag_bw": {
"unicode": "1F1E7-1F1FC",
@@ -4352,9 +8922,15 @@
"name": "botswana",
"shortname": ":flag_bw:",
"category": "flags",
- "aliases": [":bw:"],
+ "aliases": [
+ ":bw:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bw"]
+ "keywords": [
+ "country",
+ "nation",
+ "bw"
+ ]
},
"flag_by": {
"unicode": "1F1E7-1F1FE",
@@ -4362,9 +8938,16 @@
"name": "belarus",
"shortname": ":flag_by:",
"category": "flags",
- "aliases": [":by:"],
+ "aliases": [
+ ":by:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "byelarus", "by"]
+ "keywords": [
+ "country",
+ "nation",
+ "byelarus",
+ "by"
+ ]
},
"flag_bz": {
"unicode": "1F1E7-1F1FF",
@@ -4372,9 +8955,15 @@
"name": "belize",
"shortname": ":flag_bz:",
"category": "flags",
- "aliases": [":bz:"],
+ "aliases": [
+ ":bz:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bz"]
+ "keywords": [
+ "country",
+ "nation",
+ "bz"
+ ]
},
"flag_ca": {
"unicode": "1F1E8-1F1E6",
@@ -4382,9 +8971,27 @@
"name": "canada",
"shortname": ":flag_ca:",
"category": "flags",
- "aliases": [":ca:"],
+ "aliases": [
+ ":ca:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ca"
+ ]
+ },
+ "flag_cc": {
+ "unicode": "1F1E8-1F1E8",
+ "unicode_alternates": "",
+ "name": "cocos (keeling) islands",
+ "shortname": ":flag_cc:",
+ "category": "flags",
+ "aliases": [
+ ":cc:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ca"]
+ "keywords": []
},
"flag_cd": {
"unicode": "1F1E8-1F1E9",
@@ -4392,9 +8999,17 @@
"name": "the democratic republic of the congo",
"shortname": ":flag_cd:",
"category": "flags",
- "aliases": [":congo:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "r&eacute;publique d&eacute;mocratique du congo", "republique democratique du congo", "cd"]
+ "aliases": [
+ ":congo:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "r&eacute;publique d&eacute;mocratique du congo",
+ "republique democratique du congo",
+ "cd"
+ ]
},
"flag_cf": {
"unicode": "1F1E8-1F1EB",
@@ -4402,9 +9017,15 @@
"name": "central african republic",
"shortname": ":flag_cf:",
"category": "flags",
- "aliases": [":cf:"],
+ "aliases": [
+ ":cf:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "cf"]
+ "keywords": [
+ "country",
+ "nation",
+ "cf"
+ ]
},
"flag_cg": {
"unicode": "1F1E8-1F1EC",
@@ -4412,9 +9033,15 @@
"name": "the republic of the congo",
"shortname": ":flag_cg:",
"category": "flags",
- "aliases": [":cg:"],
+ "aliases": [
+ ":cg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "cg"]
+ "keywords": [
+ "country",
+ "nation",
+ "cg"
+ ]
},
"flag_ch": {
"unicode": "1F1E8-1F1ED",
@@ -4422,9 +9049,15 @@
"name": "switzerland",
"shortname": ":flag_ch:",
"category": "flags",
- "aliases": [":ch:"],
+ "aliases": [
+ ":ch:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "swiss"]
+ "keywords": [
+ "country",
+ "nation",
+ "swiss"
+ ]
},
"flag_ci": {
"unicode": "1F1E8-1F1EE",
@@ -4432,9 +9065,27 @@
"name": "cote d'ivoire",
"shortname": ":flag_ci:",
"category": "flags",
- "aliases": [":ci:"],
+ "aliases": [
+ ":ci:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ci"
+ ]
+ },
+ "flag_ck": {
+ "unicode": "1F1E8-1F1F0",
+ "unicode_alternates": "",
+ "name": "cook islands",
+ "shortname": ":flag_ck:",
+ "category": "flags",
+ "aliases": [
+ ":ck:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ci"]
+ "keywords": []
},
"flag_cl": {
"unicode": "1F1E8-1F1F1",
@@ -4442,9 +9093,15 @@
"name": "chile",
"shortname": ":flag_cl:",
"category": "flags",
- "aliases": [":chile:"],
+ "aliases": [
+ ":chile:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "cl"]
+ "keywords": [
+ "country",
+ "nation",
+ "cl"
+ ]
},
"flag_cm": {
"unicode": "1F1E8-1F1F2",
@@ -4452,9 +9109,15 @@
"name": "cameroon",
"shortname": ":flag_cm:",
"category": "flags",
- "aliases": [":cm:"],
+ "aliases": [
+ ":cm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "cm"]
+ "keywords": [
+ "country",
+ "nation",
+ "cm"
+ ]
},
"flag_cn": {
"unicode": "1F1E8-1F1F3",
@@ -4462,9 +9125,18 @@
"name": "china",
"shortname": ":flag_cn:",
"category": "flags",
- "aliases": [":cn:"],
- "aliases_ascii": [],
- "keywords": ["chinese", "prc", "zhong guo", "country", "nation", "cn"]
+ "aliases": [
+ ":cn:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "chinese",
+ "prc",
+ "zhong guo",
+ "country",
+ "nation",
+ "cn"
+ ]
},
"flag_co": {
"unicode": "1F1E8-1F1F4",
@@ -4472,9 +9144,27 @@
"name": "colombia",
"shortname": ":flag_co:",
"category": "flags",
- "aliases": [":co:"],
+ "aliases": [
+ ":co:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "co"
+ ]
+ },
+ "flag_cp": {
+ "unicode": "1F1E8-1F1F5",
+ "unicode_alternates": "",
+ "name": "clipperton island",
+ "shortname": ":flag_cp:",
+ "category": "flags",
+ "aliases": [
+ ":cp:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "co"]
+ "keywords": []
},
"flag_cr": {
"unicode": "1F1E8-1F1F7",
@@ -4482,9 +9172,15 @@
"name": "costa rica",
"shortname": ":flag_cr:",
"category": "flags",
- "aliases": [":cr:"],
+ "aliases": [
+ ":cr:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "cr"]
+ "keywords": [
+ "country",
+ "nation",
+ "cr"
+ ]
},
"flag_cu": {
"unicode": "1F1E8-1F1FA",
@@ -4492,9 +9188,15 @@
"name": "cuba",
"shortname": ":flag_cu:",
"category": "flags",
- "aliases": [":cu:"],
+ "aliases": [
+ ":cu:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "cu"]
+ "keywords": [
+ "country",
+ "nation",
+ "cu"
+ ]
},
"flag_cv": {
"unicode": "1F1E8-1F1FB",
@@ -4502,9 +9204,40 @@
"name": "cape verde",
"shortname": ":flag_cv:",
"category": "flags",
- "aliases": [":cv:"],
+ "aliases": [
+ ":cv:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "cabo verde",
+ "cv"
+ ]
+ },
+ "flag_cw": {
+ "unicode": "1F1E8-1F1FC",
+ "unicode_alternates": "",
+ "name": "curaçao",
+ "shortname": ":flag_cw:",
+ "category": "flags",
+ "aliases": [
+ ":cw:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "flag_cx": {
+ "unicode": "1F1E8-1F1FD",
+ "unicode_alternates": "",
+ "name": "christmas island",
+ "shortname": ":flag_cx:",
+ "category": "flags",
+ "aliases": [
+ ":cx:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "cabo verde", "cv"]
+ "keywords": []
},
"flag_cy": {
"unicode": "1F1E8-1F1FE",
@@ -4512,9 +9245,17 @@
"name": "cyprus",
"shortname": ":flag_cy:",
"category": "flags",
- "aliases": [":cy:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "kibris", "kypros", "cy"]
+ "aliases": [
+ ":cy:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "kibris",
+ "kypros",
+ "cy"
+ ]
},
"flag_cz": {
"unicode": "1F1E8-1F1FF",
@@ -4522,9 +9263,16 @@
"name": "the czech republic",
"shortname": ":flag_cz:",
"category": "flags",
- "aliases": [":cz:"],
+ "aliases": [
+ ":cz:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ceska republika", "cz"]
+ "keywords": [
+ "country",
+ "nation",
+ "ceska republika",
+ "cz"
+ ]
},
"flag_de": {
"unicode": "1F1E9-1F1EA",
@@ -4532,9 +9280,29 @@
"name": "germany",
"shortname": ":flag_de:",
"category": "flags",
- "aliases": [":de:"],
+ "aliases": [
+ ":de:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "german",
+ "nation",
+ "deutschland",
+ "country",
+ "de"
+ ]
+ },
+ "flag_dg": {
+ "unicode": "1F1E9-1F1EC",
+ "unicode_alternates": "",
+ "name": "diego garcia",
+ "shortname": ":flag_dg:",
+ "category": "flags",
+ "aliases": [
+ ":dg:"
+ ],
"aliases_ascii": [],
- "keywords": ["german", "nation", "deutschland", "country", "de"]
+ "keywords": []
},
"flag_dj": {
"unicode": "1F1E9-1F1EF",
@@ -4542,9 +9310,15 @@
"name": "djibouti",
"shortname": ":flag_dj:",
"category": "flags",
- "aliases": [":dj:"],
+ "aliases": [
+ ":dj:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "dj"]
+ "keywords": [
+ "country",
+ "nation",
+ "dj"
+ ]
},
"flag_dk": {
"unicode": "1F1E9-1F1F0",
@@ -4552,9 +9326,16 @@
"name": "denmark",
"shortname": ":flag_dk:",
"category": "flags",
- "aliases": [":dk:"],
+ "aliases": [
+ ":dk:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "danmark", "dk"]
+ "keywords": [
+ "country",
+ "nation",
+ "danmark",
+ "dk"
+ ]
},
"flag_dm": {
"unicode": "1F1E9-1F1F2",
@@ -4562,9 +9343,15 @@
"name": "dominica",
"shortname": ":flag_dm:",
"category": "flags",
- "aliases": [":dm:"],
+ "aliases": [
+ ":dm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "dm"]
+ "keywords": [
+ "country",
+ "nation",
+ "dm"
+ ]
},
"flag_do": {
"unicode": "1F1E9-1F1F4",
@@ -4572,9 +9359,15 @@
"name": "the dominican republic",
"shortname": ":flag_do:",
"category": "flags",
- "aliases": [":do:"],
+ "aliases": [
+ ":do:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "do"]
+ "keywords": [
+ "country",
+ "nation",
+ "do"
+ ]
},
"flag_dz": {
"unicode": "1F1E9-1F1FF",
@@ -4582,9 +9375,29 @@
"name": "algeria",
"shortname": ":flag_dz:",
"category": "flags",
- "aliases": [":dz:"],
+ "aliases": [
+ ":dz:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "al jaza'ir",
+ "al jazair",
+ "dz"
+ ]
+ },
+ "flag_ea": {
+ "unicode": "1F1EA-1F1E6",
+ "unicode_alternates": "",
+ "name": "ceuta, melilla",
+ "shortname": ":flag_ea:",
+ "category": "flags",
+ "aliases": [
+ ":ea:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "al jaza'ir", "al jazair", "dz"]
+ "keywords": []
},
"flag_ec": {
"unicode": "1F1EA-1F1E8",
@@ -4592,9 +9405,15 @@
"name": "ecuador",
"shortname": ":flag_ec:",
"category": "flags",
- "aliases": [":ec:"],
+ "aliases": [
+ ":ec:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ec"]
+ "keywords": [
+ "country",
+ "nation",
+ "ec"
+ ]
},
"flag_ee": {
"unicode": "1F1EA-1F1EA",
@@ -4602,9 +9421,16 @@
"name": "estonia",
"shortname": ":flag_ee:",
"category": "flags",
- "aliases": [":ee:"],
+ "aliases": [
+ ":ee:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "eesti vabariik", "ee"]
+ "keywords": [
+ "country",
+ "nation",
+ "eesti vabariik",
+ "ee"
+ ]
},
"flag_eg": {
"unicode": "1F1EA-1F1EC",
@@ -4612,9 +9438,16 @@
"name": "egypt",
"shortname": ":flag_eg:",
"category": "flags",
- "aliases": [":eg:"],
+ "aliases": [
+ ":eg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "misr", "eg"]
+ "keywords": [
+ "country",
+ "nation",
+ "misr",
+ "eg"
+ ]
},
"flag_eh": {
"unicode": "1F1EA-1F1ED",
@@ -4622,9 +9455,18 @@
"name": "western sahara",
"shortname": ":flag_eh:",
"category": "flags",
- "aliases": [":eh:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "aṣ-Ṣaḥrā’ al-gharbīyah", "sahra", "gharbiyah", "eh"]
+ "aliases": [
+ ":eh:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "aṣ-Ṣaḥrā’ al-gharbīyah",
+ "sahra",
+ "gharbiyah",
+ "eh"
+ ]
},
"flag_er": {
"unicode": "1F1EA-1F1F7",
@@ -4632,9 +9474,16 @@
"name": "eritrea",
"shortname": ":flag_er:",
"category": "flags",
- "aliases": [":er:"],
+ "aliases": [
+ ":er:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "hagere ertra", "er"]
+ "keywords": [
+ "country",
+ "nation",
+ "hagere ertra",
+ "er"
+ ]
},
"flag_es": {
"unicode": "1F1EA-1F1F8",
@@ -4642,9 +9491,17 @@
"name": "spain",
"shortname": ":flag_es:",
"category": "flags",
- "aliases": [":es:"],
- "aliases_ascii": [],
- "keywords": ["nation", "espa&ntilde;a", "country", "espana", "es"]
+ "aliases": [
+ ":es:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "nation",
+ "espa&ntilde;a",
+ "country",
+ "espana",
+ "es"
+ ]
},
"flag_et": {
"unicode": "1F1EA-1F1F9",
@@ -4652,9 +9509,29 @@
"name": "ethiopia",
"shortname": ":flag_et:",
"category": "flags",
- "aliases": [":et:"],
+ "aliases": [
+ ":et:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ityop'iya",
+ "ityopiya",
+ "et"
+ ]
+ },
+ "flag_eu": {
+ "unicode": "1F1EA-1F1FA",
+ "unicode_alternates": "",
+ "name": "european union",
+ "shortname": ":flag_eu:",
+ "category": "flags",
+ "aliases": [
+ ":eu:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ityop'iya", "ityopiya", "et"]
+ "keywords": []
},
"flag_fi": {
"unicode": "1F1EB-1F1EE",
@@ -4662,9 +9539,16 @@
"name": "finland",
"shortname": ":flag_fi:",
"category": "flags",
- "aliases": [":fi:"],
+ "aliases": [
+ ":fi:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "suomen tasavalta", "fi"]
+ "keywords": [
+ "country",
+ "nation",
+ "suomen tasavalta",
+ "fi"
+ ]
},
"flag_fj": {
"unicode": "1F1EB-1F1EF",
@@ -4672,9 +9556,15 @@
"name": "fiji",
"shortname": ":flag_fj:",
"category": "flags",
- "aliases": [":fj:"],
+ "aliases": [
+ ":fj:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "fj"]
+ "keywords": [
+ "country",
+ "nation",
+ "fj"
+ ]
},
"flag_fk": {
"unicode": "1F1EB-1F1F0",
@@ -4682,9 +9572,16 @@
"name": "falkland islands",
"shortname": ":flag_fk:",
"category": "flags",
- "aliases": [":fk:"],
+ "aliases": [
+ ":fk:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "islas malvinas", "fk"]
+ "keywords": [
+ "country",
+ "nation",
+ "islas malvinas",
+ "fk"
+ ]
},
"flag_fm": {
"unicode": "1F1EB-1F1F2",
@@ -4692,9 +9589,15 @@
"name": "micronesia",
"shortname": ":flag_fm:",
"category": "flags",
- "aliases": [":fm:"],
+ "aliases": [
+ ":fm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "fm"]
+ "keywords": [
+ "country",
+ "nation",
+ "fm"
+ ]
},
"flag_fo": {
"unicode": "1F1EB-1F1F4",
@@ -4702,9 +9605,16 @@
"name": "faroe islands",
"shortname": ":flag_fo:",
"category": "flags",
- "aliases": [":fo:"],
+ "aliases": [
+ ":fo:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "foroyar", "fo"]
+ "keywords": [
+ "country",
+ "nation",
+ "foroyar",
+ "fo"
+ ]
},
"flag_fr": {
"unicode": "1F1EB-1F1F7",
@@ -4712,9 +9622,16 @@
"name": "france",
"shortname": ":flag_fr:",
"category": "flags",
- "aliases": [":fr:"],
+ "aliases": [
+ ":fr:"
+ ],
"aliases_ascii": [],
- "keywords": ["french", "nation", "country", "fr"]
+ "keywords": [
+ "french",
+ "nation",
+ "country",
+ "fr"
+ ]
},
"flag_ga": {
"unicode": "1F1EC-1F1E6",
@@ -4722,9 +9639,15 @@
"name": "gabon",
"shortname": ":flag_ga:",
"category": "flags",
- "aliases": [":ga:"],
+ "aliases": [
+ ":ga:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ga"]
+ "keywords": [
+ "country",
+ "nation",
+ "ga"
+ ]
},
"flag_gb": {
"unicode": "1F1EC-1F1E7",
@@ -4732,9 +9655,19 @@
"name": "great britain",
"shortname": ":flag_gb:",
"category": "flags",
- "aliases": [":gb:"],
- "aliases_ascii": [],
- "keywords": ["UK", "gb", "britsh", "nation", "united kingdom", "england", "country"]
+ "aliases": [
+ ":gb:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "UK",
+ "gb",
+ "britsh",
+ "nation",
+ "united kingdom",
+ "england",
+ "country"
+ ]
},
"flag_gd": {
"unicode": "1F1EC-1F1E9",
@@ -4742,9 +9675,15 @@
"name": "grenada",
"shortname": ":flag_gd:",
"category": "flags",
- "aliases": [":gd:"],
+ "aliases": [
+ ":gd:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "gd"]
+ "keywords": [
+ "country",
+ "nation",
+ "gd"
+ ]
},
"flag_ge": {
"unicode": "1F1EC-1F1EA",
@@ -4752,9 +9691,41 @@
"name": "georgia",
"shortname": ":flag_ge:",
"category": "flags",
- "aliases": [":ge:"],
+ "aliases": [
+ ":ge:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "sak'art'velo",
+ "sakartvelo",
+ "ge"
+ ]
+ },
+ "flag_gf": {
+ "unicode": "1F1EC-1F1EB",
+ "unicode_alternates": "",
+ "name": "french guiana",
+ "shortname": ":flag_gf:",
+ "category": "flags",
+ "aliases": [
+ ":gf:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "flag_gg": {
+ "unicode": "1F1EC-1F1EC",
+ "unicode_alternates": "",
+ "name": "guernsey",
+ "shortname": ":flag_gg:",
+ "category": "flags",
+ "aliases": [
+ ":gg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sak'art'velo", "sakartvelo", "ge"]
+ "keywords": []
},
"flag_gh": {
"unicode": "1F1EC-1F1ED",
@@ -4762,9 +9733,15 @@
"name": "ghana",
"shortname": ":flag_gh:",
"category": "flags",
- "aliases": [":gh:"],
+ "aliases": [
+ ":gh:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "gh"]
+ "keywords": [
+ "country",
+ "nation",
+ "gh"
+ ]
},
"flag_gi": {
"unicode": "1F1EC-1F1EE",
@@ -4772,9 +9749,15 @@
"name": "gibraltar",
"shortname": ":flag_gi:",
"category": "flags",
- "aliases": [":gi:"],
+ "aliases": [
+ ":gi:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "gi"]
+ "keywords": [
+ "country",
+ "nation",
+ "gi"
+ ]
},
"flag_gl": {
"unicode": "1F1EC-1F1F1",
@@ -4782,9 +9765,16 @@
"name": "greenland",
"shortname": ":flag_gl:",
"category": "flags",
- "aliases": [":gl:"],
+ "aliases": [
+ ":gl:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "kalaallit nunaat", "gl"]
+ "keywords": [
+ "country",
+ "nation",
+ "kalaallit nunaat",
+ "gl"
+ ]
},
"flag_gm": {
"unicode": "1F1EC-1F1F2",
@@ -4792,9 +9782,15 @@
"name": "the gambia",
"shortname": ":flag_gm:",
"category": "flags",
- "aliases": [":gm:"],
+ "aliases": [
+ ":gm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "gm"]
+ "keywords": [
+ "country",
+ "nation",
+ "gm"
+ ]
},
"flag_gn": {
"unicode": "1F1EC-1F1F3",
@@ -4802,9 +9798,28 @@
"name": "guinea",
"shortname": ":flag_gn:",
"category": "flags",
- "aliases": [":gn:"],
+ "aliases": [
+ ":gn:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "guinee",
+ "gn"
+ ]
+ },
+ "flag_gp": {
+ "unicode": "1F1EC-1F1F5",
+ "unicode_alternates": "",
+ "name": "guadeloupe",
+ "shortname": ":flag_gp:",
+ "category": "flags",
+ "aliases": [
+ ":gp:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "guinee", "gn"]
+ "keywords": []
},
"flag_gq": {
"unicode": "1F1EC-1F1F6",
@@ -4812,9 +9827,16 @@
"name": "equatorial guinea",
"shortname": ":flag_gq:",
"category": "flags",
- "aliases": [":gq:"],
+ "aliases": [
+ ":gq:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "guinea ecuatorial", "gq"]
+ "keywords": [
+ "country",
+ "nation",
+ "guinea ecuatorial",
+ "gq"
+ ]
},
"flag_gr": {
"unicode": "1F1EC-1F1F7",
@@ -4822,9 +9844,29 @@
"name": "greece",
"shortname": ":flag_gr:",
"category": "flags",
- "aliases": [":gr:"],
+ "aliases": [
+ ":gr:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ellas",
+ "ellada",
+ "gr"
+ ]
+ },
+ "flag_gs": {
+ "unicode": "1F1EC-1F1F8",
+ "unicode_alternates": "",
+ "name": "south georgia",
+ "shortname": ":flag_gs:",
+ "category": "flags",
+ "aliases": [
+ ":gs:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ellas", "ellada", "gr"]
+ "keywords": []
},
"flag_gt": {
"unicode": "1F1EC-1F1F9",
@@ -4832,9 +9874,15 @@
"name": "guatemala",
"shortname": ":flag_gt:",
"category": "flags",
- "aliases": [":gt:"],
+ "aliases": [
+ ":gt:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "gt"]
+ "keywords": [
+ "country",
+ "nation",
+ "gt"
+ ]
},
"flag_gu": {
"unicode": "1F1EC-1F1FA",
@@ -4842,9 +9890,15 @@
"name": "guam",
"shortname": ":flag_gu:",
"category": "flags",
- "aliases": [":gu:"],
+ "aliases": [
+ ":gu:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "gu"]
+ "keywords": [
+ "country",
+ "nation",
+ "gu"
+ ]
},
"flag_gw": {
"unicode": "1F1EC-1F1FC",
@@ -4852,9 +9906,17 @@
"name": "guinea-bissau",
"shortname": ":flag_gw:",
"category": "flags",
- "aliases": [":gw:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "guine-bissau", "guine bissau", "gw"]
+ "aliases": [
+ ":gw:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "guine-bissau",
+ "guine bissau",
+ "gw"
+ ]
},
"flag_gy": {
"unicode": "1F1EC-1F1FE",
@@ -4862,9 +9924,15 @@
"name": "guyana",
"shortname": ":flag_gy:",
"category": "flags",
- "aliases": [":gy:"],
+ "aliases": [
+ ":gy:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "gy"]
+ "keywords": [
+ "country",
+ "nation",
+ "gy"
+ ]
},
"flag_hk": {
"unicode": "1F1ED-1F1F0",
@@ -4872,9 +9940,28 @@
"name": "hong kong",
"shortname": ":flag_hk:",
"category": "flags",
- "aliases": [":hk:"],
+ "aliases": [
+ ":hk:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "xianggang",
+ "hk"
+ ]
+ },
+ "flag_hm": {
+ "unicode": "1F1ED-1F1F2",
+ "unicode_alternates": "",
+ "name": "heard island and mcdonald islands",
+ "shortname": ":flag_hm:",
+ "category": "flags",
+ "aliases": [
+ ":hm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "xianggang", "hk"]
+ "keywords": []
},
"flag_hn": {
"unicode": "1F1ED-1F1F3",
@@ -4882,9 +9969,15 @@
"name": "honduras",
"shortname": ":flag_hn:",
"category": "flags",
- "aliases": [":hn:"],
+ "aliases": [
+ ":hn:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "hn"]
+ "keywords": [
+ "country",
+ "nation",
+ "hn"
+ ]
},
"flag_hr": {
"unicode": "1F1ED-1F1F7",
@@ -4892,9 +9985,16 @@
"name": "croatia",
"shortname": ":flag_hr:",
"category": "flags",
- "aliases": [":hr:"],
+ "aliases": [
+ ":hr:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "hrvatska", "hr"]
+ "keywords": [
+ "country",
+ "nation",
+ "hrvatska",
+ "hr"
+ ]
},
"flag_ht": {
"unicode": "1F1ED-1F1F9",
@@ -4902,9 +10002,15 @@
"name": "haiti",
"shortname": ":flag_ht:",
"category": "flags",
- "aliases": [":ht:"],
+ "aliases": [
+ ":ht:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ht"]
+ "keywords": [
+ "country",
+ "nation",
+ "ht"
+ ]
},
"flag_hu": {
"unicode": "1F1ED-1F1FA",
@@ -4912,9 +10018,28 @@
"name": "hungary",
"shortname": ":flag_hu:",
"category": "flags",
- "aliases": [":hu:"],
+ "aliases": [
+ ":hu:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "magyarorszag",
+ "hu"
+ ]
+ },
+ "flag_ic": {
+ "unicode": "1F1EE-1F1E8",
+ "unicode_alternates": "",
+ "name": "canary islands",
+ "shortname": ":flag_ic:",
+ "category": "flags",
+ "aliases": [
+ ":ic:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "magyarorszag", "hu"]
+ "keywords": []
},
"flag_id": {
"unicode": "1F1EE-1F1E9",
@@ -4922,9 +10047,15 @@
"name": "indonesia",
"shortname": ":flag_id:",
"category": "flags",
- "aliases": [":indonesia:"],
+ "aliases": [
+ ":indonesia:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "id"]
+ "keywords": [
+ "country",
+ "nation",
+ "id"
+ ]
},
"flag_ie": {
"unicode": "1F1EE-1F1EA",
@@ -4932,9 +10063,17 @@
"name": "ireland",
"shortname": ":flag_ie:",
"category": "flags",
- "aliases": [":ie:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "&eacute;ire", "eire", "ie"]
+ "aliases": [
+ ":ie:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "&eacute;ire",
+ "eire",
+ "ie"
+ ]
},
"flag_il": {
"unicode": "1F1EE-1F1F1",
@@ -4942,9 +10081,29 @@
"name": "israel",
"shortname": ":flag_il:",
"category": "flags",
- "aliases": [":il:"],
+ "aliases": [
+ ":il:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "yisra'el",
+ "yisrael",
+ "il"
+ ]
+ },
+ "flag_im": {
+ "unicode": "1F1EE-1F1F2",
+ "unicode_alternates": "",
+ "name": "isle of man",
+ "shortname": ":flag_im:",
+ "category": "flags",
+ "aliases": [
+ ":im:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "yisra'el", "yisrael", "il"]
+ "keywords": []
},
"flag_in": {
"unicode": "1F1EE-1F1F3",
@@ -4952,9 +10111,28 @@
"name": "india",
"shortname": ":flag_in:",
"category": "flags",
- "aliases": [":in:"],
+ "aliases": [
+ ":in:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "bharat",
+ "in"
+ ]
+ },
+ "flag_io": {
+ "unicode": "1F1EE-1F1F4",
+ "unicode_alternates": "",
+ "name": "british indian ocean territory",
+ "shortname": ":flag_io:",
+ "category": "flags",
+ "aliases": [
+ ":io:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "bharat", "in"]
+ "keywords": []
},
"flag_iq": {
"unicode": "1F1EE-1F1F6",
@@ -4962,9 +10140,15 @@
"name": "iraq",
"shortname": ":flag_iq:",
"category": "flags",
- "aliases": [":iq:"],
+ "aliases": [
+ ":iq:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "iq"]
+ "keywords": [
+ "country",
+ "nation",
+ "iq"
+ ]
},
"flag_ir": {
"unicode": "1F1EE-1F1F7",
@@ -4972,9 +10156,15 @@
"name": "iran",
"shortname": ":flag_ir:",
"category": "flags",
- "aliases": [":ir:"],
+ "aliases": [
+ ":ir:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ir"]
+ "keywords": [
+ "country",
+ "nation",
+ "ir"
+ ]
},
"flag_is": {
"unicode": "1F1EE-1F1F8",
@@ -4982,9 +10172,16 @@
"name": "iceland",
"shortname": ":flag_is:",
"category": "flags",
- "aliases": [":is:"],
+ "aliases": [
+ ":is:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "lyoveldio island", "is"]
+ "keywords": [
+ "country",
+ "nation",
+ "lyoveldio island",
+ "is"
+ ]
},
"flag_it": {
"unicode": "1F1EE-1F1F9",
@@ -4992,9 +10189,16 @@
"name": "italy",
"shortname": ":flag_it:",
"category": "flags",
- "aliases": [":it:"],
+ "aliases": [
+ ":it:"
+ ],
"aliases_ascii": [],
- "keywords": ["italia", "country", "nation", "it"]
+ "keywords": [
+ "italia",
+ "country",
+ "nation",
+ "it"
+ ]
},
"flag_je": {
"unicode": "1F1EF-1F1EA",
@@ -5002,9 +10206,15 @@
"name": "jersey",
"shortname": ":flag_je:",
"category": "flags",
- "aliases": [":je:"],
+ "aliases": [
+ ":je:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "je"]
+ "keywords": [
+ "country",
+ "nation",
+ "je"
+ ]
},
"flag_jm": {
"unicode": "1F1EF-1F1F2",
@@ -5012,9 +10222,15 @@
"name": "jamaica",
"shortname": ":flag_jm:",
"category": "flags",
- "aliases": [":jm:"],
+ "aliases": [
+ ":jm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "jm"]
+ "keywords": [
+ "country",
+ "nation",
+ "jm"
+ ]
},
"flag_jo": {
"unicode": "1F1EF-1F1F4",
@@ -5022,9 +10238,16 @@
"name": "jordan",
"shortname": ":flag_jo:",
"category": "flags",
- "aliases": [":jo:"],
+ "aliases": [
+ ":jo:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "al urdun", "jo"]
+ "keywords": [
+ "country",
+ "nation",
+ "al urdun",
+ "jo"
+ ]
},
"flag_jp": {
"unicode": "1F1EF-1F1F5",
@@ -5032,9 +10255,16 @@
"name": "japan",
"shortname": ":flag_jp:",
"category": "flags",
- "aliases": [":jp:"],
+ "aliases": [
+ ":jp:"
+ ],
"aliases_ascii": [],
- "keywords": ["nation", "nippon", "country", "jp"]
+ "keywords": [
+ "nation",
+ "nippon",
+ "country",
+ "jp"
+ ]
},
"flag_ke": {
"unicode": "1F1F0-1F1EA",
@@ -5042,9 +10272,15 @@
"name": "kenya",
"shortname": ":flag_ke:",
"category": "flags",
- "aliases": [":ke:"],
+ "aliases": [
+ ":ke:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ke"]
+ "keywords": [
+ "country",
+ "nation",
+ "ke"
+ ]
},
"flag_kg": {
"unicode": "1F1F0-1F1EC",
@@ -5052,9 +10288,16 @@
"name": "kyrgyzstan",
"shortname": ":flag_kg:",
"category": "flags",
- "aliases": [":kg:"],
+ "aliases": [
+ ":kg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "kyrgyz respublikasy", "kg"]
+ "keywords": [
+ "country",
+ "nation",
+ "kyrgyz respublikasy",
+ "kg"
+ ]
},
"flag_kh": {
"unicode": "1F1F0-1F1ED",
@@ -5062,9 +10305,16 @@
"name": "cambodia",
"shortname": ":flag_kh:",
"category": "flags",
- "aliases": [":kh:"],
+ "aliases": [
+ ":kh:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "kampuchea", "kh"]
+ "keywords": [
+ "country",
+ "nation",
+ "kampuchea",
+ "kh"
+ ]
},
"flag_ki": {
"unicode": "1F1F0-1F1EE",
@@ -5072,9 +10322,17 @@
"name": "kiribati",
"shortname": ":flag_ki:",
"category": "flags",
- "aliases": [":ki:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "kiribati", "kiribas", "ki"]
+ "aliases": [
+ ":ki:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "kiribati",
+ "kiribas",
+ "ki"
+ ]
},
"flag_km": {
"unicode": "1F1F0-1F1F2",
@@ -5082,9 +10340,15 @@
"name": "the comoros",
"shortname": ":flag_km:",
"category": "flags",
- "aliases": [":km:"],
+ "aliases": [
+ ":km:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "km"]
+ "keywords": [
+ "country",
+ "nation",
+ "km"
+ ]
},
"flag_kn": {
"unicode": "1F1F0-1F1F3",
@@ -5092,9 +10356,15 @@
"name": "saint kitts and nevis",
"shortname": ":flag_kn:",
"category": "flags",
- "aliases": [":kn:"],
+ "aliases": [
+ ":kn:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "kn"]
+ "keywords": [
+ "country",
+ "nation",
+ "kn"
+ ]
},
"flag_kp": {
"unicode": "1F1F0-1F1F5",
@@ -5102,9 +10372,15 @@
"name": "north korea",
"shortname": ":flag_kp:",
"category": "flags",
- "aliases": [":kp:"],
+ "aliases": [
+ ":kp:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "kp"]
+ "keywords": [
+ "country",
+ "nation",
+ "kp"
+ ]
},
"flag_kr": {
"unicode": "1F1F0-1F1F7",
@@ -5112,9 +10388,16 @@
"name": "korea",
"shortname": ":flag_kr:",
"category": "flags",
- "aliases": [":kr:"],
+ "aliases": [
+ ":kr:"
+ ],
"aliases_ascii": [],
- "keywords": ["nation", "country", "south korea", "kr"]
+ "keywords": [
+ "nation",
+ "country",
+ "south korea",
+ "kr"
+ ]
},
"flag_kw": {
"unicode": "1F1F0-1F1FC",
@@ -5122,9 +10405,16 @@
"name": "kuwait",
"shortname": ":flag_kw:",
"category": "flags",
- "aliases": [":kw:"],
+ "aliases": [
+ ":kw:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "al kuwayt", "kw"]
+ "keywords": [
+ "country",
+ "nation",
+ "al kuwayt",
+ "kw"
+ ]
},
"flag_ky": {
"unicode": "1F1F0-1F1FE",
@@ -5132,9 +10422,15 @@
"name": "cayman islands",
"shortname": ":flag_ky:",
"category": "flags",
- "aliases": [":ky:"],
+ "aliases": [
+ ":ky:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ky"]
+ "keywords": [
+ "country",
+ "nation",
+ "ky"
+ ]
},
"flag_kz": {
"unicode": "1F1F0-1F1FF",
@@ -5142,9 +10438,16 @@
"name": "kazakhstan",
"shortname": ":flag_kz:",
"category": "flags",
- "aliases": [":kz:"],
+ "aliases": [
+ ":kz:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "qazaqstan", "kz"]
+ "keywords": [
+ "country",
+ "nation",
+ "qazaqstan",
+ "kz"
+ ]
},
"flag_la": {
"unicode": "1F1F1-1F1E6",
@@ -5152,9 +10455,15 @@
"name": "laos",
"shortname": ":flag_la:",
"category": "flags",
- "aliases": [":la:"],
+ "aliases": [
+ ":la:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "la"]
+ "keywords": [
+ "country",
+ "nation",
+ "la"
+ ]
},
"flag_lb": {
"unicode": "1F1F1-1F1E7",
@@ -5162,9 +10471,16 @@
"name": "lebanon",
"shortname": ":flag_lb:",
"category": "flags",
- "aliases": [":lb:"],
+ "aliases": [
+ ":lb:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "lubnan", "lb"]
+ "keywords": [
+ "country",
+ "nation",
+ "lubnan",
+ "lb"
+ ]
},
"flag_lc": {
"unicode": "1F1F1-1F1E8",
@@ -5172,9 +10488,15 @@
"name": "saint lucia",
"shortname": ":flag_lc:",
"category": "flags",
- "aliases": [":lc:"],
+ "aliases": [
+ ":lc:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "lc"]
+ "keywords": [
+ "country",
+ "nation",
+ "lc"
+ ]
},
"flag_li": {
"unicode": "1F1F1-1F1EE",
@@ -5182,9 +10504,15 @@
"name": "liechtenstein",
"shortname": ":flag_li:",
"category": "flags",
- "aliases": [":li:"],
+ "aliases": [
+ ":li:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "li"]
+ "keywords": [
+ "country",
+ "nation",
+ "li"
+ ]
},
"flag_lk": {
"unicode": "1F1F1-1F1F0",
@@ -5192,9 +10520,15 @@
"name": "sri lanka",
"shortname": ":flag_lk:",
"category": "flags",
- "aliases": [":lk:"],
+ "aliases": [
+ ":lk:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "lk"]
+ "keywords": [
+ "country",
+ "nation",
+ "lk"
+ ]
},
"flag_lr": {
"unicode": "1F1F1-1F1F7",
@@ -5202,9 +10536,15 @@
"name": "liberia",
"shortname": ":flag_lr:",
"category": "flags",
- "aliases": [":lr:"],
+ "aliases": [
+ ":lr:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "lr"]
+ "keywords": [
+ "country",
+ "nation",
+ "lr"
+ ]
},
"flag_ls": {
"unicode": "1F1F1-1F1F8",
@@ -5212,9 +10552,15 @@
"name": "lesotho",
"shortname": ":flag_ls:",
"category": "flags",
- "aliases": [":ls:"],
+ "aliases": [
+ ":ls:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ls"]
+ "keywords": [
+ "country",
+ "nation",
+ "ls"
+ ]
},
"flag_lt": {
"unicode": "1F1F1-1F1F9",
@@ -5222,9 +10568,16 @@
"name": "lithuania",
"shortname": ":flag_lt:",
"category": "flags",
- "aliases": [":lt:"],
+ "aliases": [
+ ":lt:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "lietuva", "lt"]
+ "keywords": [
+ "country",
+ "nation",
+ "lietuva",
+ "lt"
+ ]
},
"flag_lu": {
"unicode": "1F1F1-1F1FA",
@@ -5232,9 +10585,17 @@
"name": "luxembourg",
"shortname": ":flag_lu:",
"category": "flags",
- "aliases": [":lu:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "luxembourg", "letzebuerg", "lu"]
+ "aliases": [
+ ":lu:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "luxembourg",
+ "letzebuerg",
+ "lu"
+ ]
},
"flag_lv": {
"unicode": "1F1F1-1F1FB",
@@ -5242,9 +10603,16 @@
"name": "latvia",
"shortname": ":flag_lv:",
"category": "flags",
- "aliases": [":lv:"],
+ "aliases": [
+ ":lv:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "latvija", "lv"]
+ "keywords": [
+ "country",
+ "nation",
+ "latvija",
+ "lv"
+ ]
},
"flag_ly": {
"unicode": "1F1F1-1F1FE",
@@ -5252,9 +10620,16 @@
"name": "libya",
"shortname": ":flag_ly:",
"category": "flags",
- "aliases": [":ly:"],
+ "aliases": [
+ ":ly:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "libiyah", "ly"]
+ "keywords": [
+ "country",
+ "nation",
+ "libiyah",
+ "ly"
+ ]
},
"flag_ma": {
"unicode": "1F1F2-1F1E6",
@@ -5262,9 +10637,16 @@
"name": "morocco",
"shortname": ":flag_ma:",
"category": "flags",
- "aliases": [":ma:"],
+ "aliases": [
+ ":ma:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "al maghrib", "ma"]
+ "keywords": [
+ "country",
+ "nation",
+ "al maghrib",
+ "ma"
+ ]
},
"flag_mc": {
"unicode": "1F1F2-1F1E8",
@@ -5272,9 +10654,15 @@
"name": "monaco",
"shortname": ":flag_mc:",
"category": "flags",
- "aliases": [":mc:"],
+ "aliases": [
+ ":mc:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mc"]
+ "keywords": [
+ "country",
+ "nation",
+ "mc"
+ ]
},
"flag_md": {
"unicode": "1F1F2-1F1E9",
@@ -5282,9 +10670,15 @@
"name": "moldova",
"shortname": ":flag_md:",
"category": "flags",
- "aliases": [":md:"],
+ "aliases": [
+ ":md:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "md"]
+ "keywords": [
+ "country",
+ "nation",
+ "md"
+ ]
},
"flag_me": {
"unicode": "1F1F2-1F1EA",
@@ -5292,9 +10686,28 @@
"name": "montenegro",
"shortname": ":flag_me:",
"category": "flags",
- "aliases": [":me:"],
+ "aliases": [
+ ":me:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "crna gora",
+ "me"
+ ]
+ },
+ "flag_mf": {
+ "unicode": "1F1F2-1F1EB",
+ "unicode_alternates": "",
+ "name": "saint martin",
+ "shortname": ":flag_mf:",
+ "category": "flags",
+ "aliases": [
+ ":mf:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "crna gora", "me"]
+ "keywords": []
},
"flag_mg": {
"unicode": "1F1F2-1F1EC",
@@ -5302,9 +10715,15 @@
"name": "madagascar",
"shortname": ":flag_mg:",
"category": "flags",
- "aliases": [":mg:"],
+ "aliases": [
+ ":mg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mg"]
+ "keywords": [
+ "country",
+ "nation",
+ "mg"
+ ]
},
"flag_mh": {
"unicode": "1F1F2-1F1ED",
@@ -5312,9 +10731,15 @@
"name": "the marshall islands",
"shortname": ":flag_mh:",
"category": "flags",
- "aliases": [":mh:"],
+ "aliases": [
+ ":mh:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mh"]
+ "keywords": [
+ "country",
+ "nation",
+ "mh"
+ ]
},
"flag_mk": {
"unicode": "1F1F2-1F1F0",
@@ -5322,9 +10747,15 @@
"name": "macedonia",
"shortname": ":flag_mk:",
"category": "flags",
- "aliases": [":mk:"],
+ "aliases": [
+ ":mk:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mk"]
+ "keywords": [
+ "country",
+ "nation",
+ "mk"
+ ]
},
"flag_ml": {
"unicode": "1F1F2-1F1F1",
@@ -5332,9 +10763,15 @@
"name": "mali",
"shortname": ":flag_ml:",
"category": "flags",
- "aliases": [":ml:"],
+ "aliases": [
+ ":ml:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ml"]
+ "keywords": [
+ "country",
+ "nation",
+ "ml"
+ ]
},
"flag_mm": {
"unicode": "1F1F2-1F1F2",
@@ -5342,9 +10779,16 @@
"name": "myanmar",
"shortname": ":flag_mm:",
"category": "flags",
- "aliases": [":mm:"],
+ "aliases": [
+ ":mm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "myanma naingngandaw", "mm"]
+ "keywords": [
+ "country",
+ "nation",
+ "myanma naingngandaw",
+ "mm"
+ ]
},
"flag_mn": {
"unicode": "1F1F2-1F1F3",
@@ -5352,9 +10796,16 @@
"name": "mongolia",
"shortname": ":flag_mn:",
"category": "flags",
- "aliases": [":mn:"],
+ "aliases": [
+ ":mn:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mongol uls", "mn"]
+ "keywords": [
+ "country",
+ "nation",
+ "mongol uls",
+ "mn"
+ ]
},
"flag_mo": {
"unicode": "1F1F2-1F1F4",
@@ -5362,9 +10813,40 @@
"name": "macau",
"shortname": ":flag_mo:",
"category": "flags",
- "aliases": [":mo:"],
+ "aliases": [
+ ":mo:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "aomen",
+ "mo"
+ ]
+ },
+ "flag_mp": {
+ "unicode": "1F1F2-1F1F5",
+ "unicode_alternates": "",
+ "name": "northern mariana islands",
+ "shortname": ":flag_mp:",
+ "category": "flags",
+ "aliases": [
+ ":mp:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "aomen", "mo"]
+ "keywords": []
+ },
+ "flag_mq": {
+ "unicode": "1F1F2-1F1F6",
+ "unicode_alternates": "",
+ "name": "martinique",
+ "shortname": ":flag_mq:",
+ "category": "flags",
+ "aliases": [
+ ":mq:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
},
"flag_mr": {
"unicode": "1F1F2-1F1F7",
@@ -5372,9 +10854,16 @@
"name": "mauritania",
"shortname": ":flag_mr:",
"category": "flags",
- "aliases": [":mr:"],
+ "aliases": [
+ ":mr:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "muritaniyah", "mr"]
+ "keywords": [
+ "country",
+ "nation",
+ "muritaniyah",
+ "mr"
+ ]
},
"flag_ms": {
"unicode": "1F1F2-1F1F8",
@@ -5382,9 +10871,15 @@
"name": "montserrat",
"shortname": ":flag_ms:",
"category": "flags",
- "aliases": [":ms:"],
+ "aliases": [
+ ":ms:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ms"]
+ "keywords": [
+ "country",
+ "nation",
+ "ms"
+ ]
},
"flag_mt": {
"unicode": "1F1F2-1F1F9",
@@ -5392,9 +10887,15 @@
"name": "malta",
"shortname": ":flag_mt:",
"category": "flags",
- "aliases": [":mt:"],
+ "aliases": [
+ ":mt:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mt"]
+ "keywords": [
+ "country",
+ "nation",
+ "mt"
+ ]
},
"flag_mu": {
"unicode": "1F1F2-1F1FA",
@@ -5402,9 +10903,15 @@
"name": "mauritius",
"shortname": ":flag_mu:",
"category": "flags",
- "aliases": [":mu:"],
+ "aliases": [
+ ":mu:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mu"]
+ "keywords": [
+ "country",
+ "nation",
+ "mu"
+ ]
},
"flag_mv": {
"unicode": "1F1F2-1F1FB",
@@ -5412,9 +10919,16 @@
"name": "maldives",
"shortname": ":flag_mv:",
"category": "flags",
- "aliases": [":mv:"],
+ "aliases": [
+ ":mv:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "dhivehi raajje", "mv"]
+ "keywords": [
+ "country",
+ "nation",
+ "dhivehi raajje",
+ "mv"
+ ]
},
"flag_mw": {
"unicode": "1F1F2-1F1FC",
@@ -5422,9 +10936,15 @@
"name": "malawi",
"shortname": ":flag_mw:",
"category": "flags",
- "aliases": [":mw:"],
+ "aliases": [
+ ":mw:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mw"]
+ "keywords": [
+ "country",
+ "nation",
+ "mw"
+ ]
},
"flag_mx": {
"unicode": "1F1F2-1F1FD",
@@ -5432,9 +10952,15 @@
"name": "mexico",
"shortname": ":flag_mx:",
"category": "flags",
- "aliases": [":mx:"],
+ "aliases": [
+ ":mx:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mx"]
+ "keywords": [
+ "country",
+ "nation",
+ "mx"
+ ]
},
"flag_my": {
"unicode": "1F1F2-1F1FE",
@@ -5442,9 +10968,15 @@
"name": "malaysia",
"shortname": ":flag_my:",
"category": "flags",
- "aliases": [":my:"],
+ "aliases": [
+ ":my:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "my"]
+ "keywords": [
+ "country",
+ "nation",
+ "my"
+ ]
},
"flag_mz": {
"unicode": "1F1F2-1F1FF",
@@ -5452,9 +10984,16 @@
"name": "mozambique",
"shortname": ":flag_mz:",
"category": "flags",
- "aliases": [":mz:"],
+ "aliases": [
+ ":mz:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "mocambique", "mz"]
+ "keywords": [
+ "country",
+ "nation",
+ "mocambique",
+ "mz"
+ ]
},
"flag_na": {
"unicode": "1F1F3-1F1E6",
@@ -5462,9 +11001,15 @@
"name": "namibia",
"shortname": ":flag_na:",
"category": "flags",
- "aliases": [":na:"],
+ "aliases": [
+ ":na:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "na"]
+ "keywords": [
+ "country",
+ "nation",
+ "na"
+ ]
},
"flag_nc": {
"unicode": "1F1F3-1F1E8",
@@ -5472,9 +11017,18 @@
"name": "new caledonia",
"shortname": ":flag_nc:",
"category": "flags",
- "aliases": [":nc:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "nouvelle", "cal&eacute;donie", "caledonie", "nc"]
+ "aliases": [
+ ":nc:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "nouvelle",
+ "cal&eacute;donie",
+ "caledonie",
+ "nc"
+ ]
},
"flag_ne": {
"unicode": "1F1F3-1F1EA",
@@ -5482,9 +11036,27 @@
"name": "niger",
"shortname": ":flag_ne:",
"category": "flags",
- "aliases": [":ne:"],
+ "aliases": [
+ ":ne:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ne"
+ ]
+ },
+ "flag_nf": {
+ "unicode": "1F1F3-1F1EB",
+ "unicode_alternates": "",
+ "name": "norfolk island",
+ "shortname": ":flag_nf:",
+ "category": "flags",
+ "aliases": [
+ ":nf:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ne"]
+ "keywords": []
},
"flag_ng": {
"unicode": "1F1F3-1F1EC",
@@ -5492,9 +11064,15 @@
"name": "nigeria",
"shortname": ":flag_ng:",
"category": "flags",
- "aliases": [":nigeria:"],
+ "aliases": [
+ ":nigeria:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ng"]
+ "keywords": [
+ "country",
+ "nation",
+ "ng"
+ ]
},
"flag_ni": {
"unicode": "1F1F3-1F1EE",
@@ -5502,9 +11080,15 @@
"name": "nicaragua",
"shortname": ":flag_ni:",
"category": "flags",
- "aliases": [":ni:"],
+ "aliases": [
+ ":ni:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ni"]
+ "keywords": [
+ "country",
+ "nation",
+ "ni"
+ ]
},
"flag_nl": {
"unicode": "1F1F3-1F1F1",
@@ -5512,9 +11096,17 @@
"name": "the netherlands",
"shortname": ":flag_nl:",
"category": "flags",
- "aliases": [":nl:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "nederland", "holland", "nl"]
+ "aliases": [
+ ":nl:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "nederland",
+ "holland",
+ "nl"
+ ]
},
"flag_no": {
"unicode": "1F1F3-1F1F4",
@@ -5522,9 +11114,16 @@
"name": "norway",
"shortname": ":flag_no:",
"category": "flags",
- "aliases": [":no:"],
+ "aliases": [
+ ":no:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "norge", "no"]
+ "keywords": [
+ "country",
+ "nation",
+ "norge",
+ "no"
+ ]
},
"flag_np": {
"unicode": "1F1F3-1F1F5",
@@ -5532,9 +11131,15 @@
"name": "nepal",
"shortname": ":flag_np:",
"category": "flags",
- "aliases": [":np:"],
+ "aliases": [
+ ":np:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "np"]
+ "keywords": [
+ "country",
+ "nation",
+ "np"
+ ]
},
"flag_nr": {
"unicode": "1F1F3-1F1F7",
@@ -5542,9 +11147,15 @@
"name": "nauru",
"shortname": ":flag_nr:",
"category": "flags",
- "aliases": [":nr:"],
+ "aliases": [
+ ":nr:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "nr"]
+ "keywords": [
+ "country",
+ "nation",
+ "nr"
+ ]
},
"flag_nu": {
"unicode": "1F1F3-1F1FA",
@@ -5552,9 +11163,15 @@
"name": "niue",
"shortname": ":flag_nu:",
"category": "flags",
- "aliases": [":nu:"],
+ "aliases": [
+ ":nu:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "nu"]
+ "keywords": [
+ "country",
+ "nation",
+ "nu"
+ ]
},
"flag_nz": {
"unicode": "1F1F3-1F1FF",
@@ -5562,9 +11179,16 @@
"name": "new zealand",
"shortname": ":flag_nz:",
"category": "flags",
- "aliases": [":nz:"],
+ "aliases": [
+ ":nz:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "aotearoa", "nz"]
+ "keywords": [
+ "country",
+ "nation",
+ "aotearoa",
+ "nz"
+ ]
},
"flag_om": {
"unicode": "1F1F4-1F1F2",
@@ -5572,9 +11196,16 @@
"name": "oman",
"shortname": ":flag_om:",
"category": "flags",
- "aliases": [":om:"],
+ "aliases": [
+ ":om:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "saltanat uman", "om"]
+ "keywords": [
+ "country",
+ "nation",
+ "saltanat uman",
+ "om"
+ ]
},
"flag_pa": {
"unicode": "1F1F5-1F1E6",
@@ -5582,9 +11213,15 @@
"name": "panama",
"shortname": ":flag_pa:",
"category": "flags",
- "aliases": [":pa:"],
+ "aliases": [
+ ":pa:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "pa"]
+ "keywords": [
+ "country",
+ "nation",
+ "pa"
+ ]
},
"flag_pe": {
"unicode": "1F1F5-1F1EA",
@@ -5592,9 +11229,15 @@
"name": "peru",
"shortname": ":flag_pe:",
"category": "flags",
- "aliases": [":pe:"],
+ "aliases": [
+ ":pe:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "pe"]
+ "keywords": [
+ "country",
+ "nation",
+ "pe"
+ ]
},
"flag_pf": {
"unicode": "1F1F5-1F1EB",
@@ -5602,9 +11245,17 @@
"name": "french polynesia",
"shortname": ":flag_pf:",
"category": "flags",
- "aliases": [":pf:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "polyn&eacute;sie fran&ccedil;aise", "polynesie francaise", "pf"]
+ "aliases": [
+ ":pf:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "polyn&eacute;sie fran&ccedil;aise",
+ "polynesie francaise",
+ "pf"
+ ]
},
"flag_pg": {
"unicode": "1F1F5-1F1EC",
@@ -5612,9 +11263,16 @@
"name": "papua new guinea",
"shortname": ":flag_pg:",
"category": "flags",
- "aliases": [":pg:"],
+ "aliases": [
+ ":pg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "papua niu gini", "pg"]
+ "keywords": [
+ "country",
+ "nation",
+ "papua niu gini",
+ "pg"
+ ]
},
"flag_ph": {
"unicode": "1F1F5-1F1ED",
@@ -5622,9 +11280,16 @@
"name": "the philippines",
"shortname": ":flag_ph:",
"category": "flags",
- "aliases": [":ph:"],
+ "aliases": [
+ ":ph:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "pilipinas", "ph"]
+ "keywords": [
+ "country",
+ "nation",
+ "pilipinas",
+ "ph"
+ ]
},
"flag_pk": {
"unicode": "1F1F5-1F1F0",
@@ -5632,9 +11297,15 @@
"name": "pakistan",
"shortname": ":flag_pk:",
"category": "flags",
- "aliases": [":pk:"],
+ "aliases": [
+ ":pk:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "pk"]
+ "keywords": [
+ "country",
+ "nation",
+ "pk"
+ ]
},
"flag_pl": {
"unicode": "1F1F5-1F1F1",
@@ -5642,9 +11313,40 @@
"name": "poland",
"shortname": ":flag_pl:",
"category": "flags",
- "aliases": [":pl:"],
+ "aliases": [
+ ":pl:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "polska",
+ "pl"
+ ]
+ },
+ "flag_pm": {
+ "unicode": "1F1F5-1F1F2",
+ "unicode_alternates": "",
+ "name": "saint pierre and miquelon",
+ "shortname": ":flag_pm:",
+ "category": "flags",
+ "aliases": [
+ ":pm:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "flag_pn": {
+ "unicode": "1F1F5-1F1F3",
+ "unicode_alternates": "",
+ "name": "pitcairn",
+ "shortname": ":flag_pn:",
+ "category": "flags",
+ "aliases": [
+ ":pn:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "polska", "pl"]
+ "keywords": []
},
"flag_pr": {
"unicode": "1F1F5-1F1F7",
@@ -5652,9 +11354,15 @@
"name": "puerto rico",
"shortname": ":flag_pr:",
"category": "flags",
- "aliases": [":pr:"],
+ "aliases": [
+ ":pr:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "pr"]
+ "keywords": [
+ "country",
+ "nation",
+ "pr"
+ ]
},
"flag_ps": {
"unicode": "1F1F5-1F1F8",
@@ -5662,9 +11370,15 @@
"name": "palestinian authority",
"shortname": ":flag_ps:",
"category": "flags",
- "aliases": [":ps:"],
+ "aliases": [
+ ":ps:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ps"]
+ "keywords": [
+ "country",
+ "nation",
+ "ps"
+ ]
},
"flag_pt": {
"unicode": "1F1F5-1F1F9",
@@ -5672,9 +11386,15 @@
"name": "portugal",
"shortname": ":flag_pt:",
"category": "flags",
- "aliases": [":pt:"],
+ "aliases": [
+ ":pt:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "pt"]
+ "keywords": [
+ "country",
+ "nation",
+ "pt"
+ ]
},
"flag_pw": {
"unicode": "1F1F5-1F1FC",
@@ -5682,9 +11402,16 @@
"name": "palau",
"shortname": ":flag_pw:",
"category": "flags",
- "aliases": [":pw:"],
+ "aliases": [
+ ":pw:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "belau", "pw"]
+ "keywords": [
+ "country",
+ "nation",
+ "belau",
+ "pw"
+ ]
},
"flag_py": {
"unicode": "1F1F5-1F1FE",
@@ -5692,9 +11419,15 @@
"name": "paraguay",
"shortname": ":flag_py:",
"category": "flags",
- "aliases": [":py:"],
+ "aliases": [
+ ":py:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "py"]
+ "keywords": [
+ "country",
+ "nation",
+ "py"
+ ]
},
"flag_qa": {
"unicode": "1F1F6-1F1E6",
@@ -5702,9 +11435,28 @@
"name": "qatar",
"shortname": ":flag_qa:",
"category": "flags",
- "aliases": [":qa:"],
+ "aliases": [
+ ":qa:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "dawlat qatar",
+ "qa"
+ ]
+ },
+ "flag_re": {
+ "unicode": "1F1F7-1F1EA",
+ "unicode_alternates": "",
+ "name": "réunion",
+ "shortname": ":flag_re:",
+ "category": "flags",
+ "aliases": [
+ ":re:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "dawlat qatar", "qa"]
+ "keywords": []
},
"flag_ro": {
"unicode": "1F1F7-1F1F4",
@@ -5712,9 +11464,15 @@
"name": "romania",
"shortname": ":flag_ro:",
"category": "flags",
- "aliases": [":ro:"],
+ "aliases": [
+ ":ro:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ro"]
+ "keywords": [
+ "country",
+ "nation",
+ "ro"
+ ]
},
"flag_rs": {
"unicode": "1F1F7-1F1F8",
@@ -5722,9 +11480,16 @@
"name": "serbia",
"shortname": ":flag_rs:",
"category": "flags",
- "aliases": [":rs:"],
+ "aliases": [
+ ":rs:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "srbija", "rs"]
+ "keywords": [
+ "country",
+ "nation",
+ "srbija",
+ "rs"
+ ]
},
"flag_ru": {
"unicode": "1F1F7-1F1FA",
@@ -5732,9 +11497,16 @@
"name": "russia",
"shortname": ":flag_ru:",
"category": "flags",
- "aliases": [":ru:"],
+ "aliases": [
+ ":ru:"
+ ],
"aliases_ascii": [],
- "keywords": ["nation", "russian", "country", "ru"]
+ "keywords": [
+ "nation",
+ "russian",
+ "country",
+ "ru"
+ ]
},
"flag_rw": {
"unicode": "1F1F7-1F1FC",
@@ -5742,9 +11514,15 @@
"name": "rwanda",
"shortname": ":flag_rw:",
"category": "flags",
- "aliases": [":rw:"],
+ "aliases": [
+ ":rw:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "rw"]
+ "keywords": [
+ "country",
+ "nation",
+ "rw"
+ ]
},
"flag_sa": {
"unicode": "1F1F8-1F1E6",
@@ -5752,9 +11530,17 @@
"name": "saudi arabia",
"shortname": ":flag_sa:",
"category": "flags",
- "aliases": [":saudiarabia:", ":saudi:"],
- "aliases_ascii": [],
- "keywords": ["country", "nation", "al arabiyah as suudiyah", "sa"]
+ "aliases": [
+ ":saudiarabia:",
+ ":saudi:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "al arabiyah as suudiyah",
+ "sa"
+ ]
},
"flag_sb": {
"unicode": "1F1F8-1F1E7",
@@ -5762,9 +11548,15 @@
"name": "the solomon islands",
"shortname": ":flag_sb:",
"category": "flags",
- "aliases": [":sb:"],
+ "aliases": [
+ ":sb:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sb"]
+ "keywords": [
+ "country",
+ "nation",
+ "sb"
+ ]
},
"flag_sc": {
"unicode": "1F1F8-1F1E8",
@@ -5772,9 +11564,16 @@
"name": "the seychelles",
"shortname": ":flag_sc:",
"category": "flags",
- "aliases": [":sc:"],
+ "aliases": [
+ ":sc:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "seychelles", "sc"]
+ "keywords": [
+ "country",
+ "nation",
+ "seychelles",
+ "sc"
+ ]
},
"flag_sd": {
"unicode": "1F1F8-1F1E9",
@@ -5782,9 +11581,16 @@
"name": "sudan",
"shortname": ":flag_sd:",
"category": "flags",
- "aliases": [":sd:"],
+ "aliases": [
+ ":sd:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "as-sudan", "sd"]
+ "keywords": [
+ "country",
+ "nation",
+ "as-sudan",
+ "sd"
+ ]
},
"flag_se": {
"unicode": "1F1F8-1F1EA",
@@ -5792,9 +11598,16 @@
"name": "sweden",
"shortname": ":flag_se:",
"category": "flags",
- "aliases": [":se:"],
+ "aliases": [
+ ":se:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sverige", "se"]
+ "keywords": [
+ "country",
+ "nation",
+ "sverige",
+ "se"
+ ]
},
"flag_sg": {
"unicode": "1F1F8-1F1EC",
@@ -5802,9 +11615,15 @@
"name": "singapore",
"shortname": ":flag_sg:",
"category": "flags",
- "aliases": [":sg:"],
+ "aliases": [
+ ":sg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sg"]
+ "keywords": [
+ "country",
+ "nation",
+ "sg"
+ ]
},
"flag_sh": {
"unicode": "1F1F8-1F1ED",
@@ -5812,9 +11631,15 @@
"name": "saint helena",
"shortname": ":flag_sh:",
"category": "flags",
- "aliases": [":sh:"],
+ "aliases": [
+ ":sh:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sh"]
+ "keywords": [
+ "country",
+ "nation",
+ "sh"
+ ]
},
"flag_si": {
"unicode": "1F1F8-1F1EE",
@@ -5822,9 +11647,28 @@
"name": "slovenia",
"shortname": ":flag_si:",
"category": "flags",
- "aliases": [":si:"],
+ "aliases": [
+ ":si:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "slovenija",
+ "si"
+ ]
+ },
+ "flag_sj": {
+ "unicode": "1F1F8-1F1EF",
+ "unicode_alternates": "",
+ "name": "svalbard and jan mayen",
+ "shortname": ":flag_sj:",
+ "category": "flags",
+ "aliases": [
+ ":sj:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "slovenija", "si"]
+ "keywords": []
},
"flag_sk": {
"unicode": "1F1F8-1F1F0",
@@ -5832,9 +11676,15 @@
"name": "slovakia",
"shortname": ":flag_sk:",
"category": "flags",
- "aliases": [":sk:"],
+ "aliases": [
+ ":sk:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sk"]
+ "keywords": [
+ "country",
+ "nation",
+ "sk"
+ ]
},
"flag_sl": {
"unicode": "1F1F8-1F1F1",
@@ -5842,9 +11692,15 @@
"name": "sierra leone",
"shortname": ":flag_sl:",
"category": "flags",
- "aliases": [":sl:"],
+ "aliases": [
+ ":sl:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sl"]
+ "keywords": [
+ "country",
+ "nation",
+ "sl"
+ ]
},
"flag_sm": {
"unicode": "1F1F8-1F1F2",
@@ -5852,9 +11708,15 @@
"name": "san marino",
"shortname": ":flag_sm:",
"category": "flags",
- "aliases": [":sm:"],
+ "aliases": [
+ ":sm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sm"]
+ "keywords": [
+ "country",
+ "nation",
+ "sm"
+ ]
},
"flag_sn": {
"unicode": "1F1F8-1F1F3",
@@ -5862,9 +11724,15 @@
"name": "senegal",
"shortname": ":flag_sn:",
"category": "flags",
- "aliases": [":sn:"],
+ "aliases": [
+ ":sn:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sn"]
+ "keywords": [
+ "country",
+ "nation",
+ "sn"
+ ]
},
"flag_so": {
"unicode": "1F1F8-1F1F4",
@@ -5872,9 +11740,15 @@
"name": "somalia",
"shortname": ":flag_so:",
"category": "flags",
- "aliases": [":so:"],
+ "aliases": [
+ ":so:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "so"]
+ "keywords": [
+ "country",
+ "nation",
+ "so"
+ ]
},
"flag_sr": {
"unicode": "1F1F8-1F1F7",
@@ -5882,9 +11756,27 @@
"name": "suriname",
"shortname": ":flag_sr:",
"category": "flags",
- "aliases": [":sr:"],
+ "aliases": [
+ ":sr:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "sr"
+ ]
+ },
+ "flag_ss": {
+ "unicode": "1F1F8-1F1F8",
+ "unicode_alternates": "",
+ "name": "south sudan",
+ "shortname": ":flag_ss:",
+ "category": "flags",
+ "aliases": [
+ ":ss:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sr"]
+ "keywords": []
},
"flag_st": {
"unicode": "1F1F8-1F1F9",
@@ -5892,9 +11784,16 @@
"name": "sao tome and principe",
"shortname": ":flag_st:",
"category": "flags",
- "aliases": [":st:"],
+ "aliases": [
+ ":st:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sao tome e principe", "st"]
+ "keywords": [
+ "country",
+ "nation",
+ "sao tome e principe",
+ "st"
+ ]
},
"flag_sv": {
"unicode": "1F1F8-1F1FB",
@@ -5902,9 +11801,27 @@
"name": "el salvador",
"shortname": ":flag_sv:",
"category": "flags",
- "aliases": [":sv:"],
+ "aliases": [
+ ":sv:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "sv"
+ ]
+ },
+ "flag_sx": {
+ "unicode": "1F1F8-1F1FD",
+ "unicode_alternates": "",
+ "name": "sint maarten",
+ "shortname": ":flag_sx:",
+ "category": "flags",
+ "aliases": [
+ ":sx:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sv"]
+ "keywords": []
},
"flag_sy": {
"unicode": "1F1F8-1F1FE",
@@ -5912,9 +11829,15 @@
"name": "syria",
"shortname": ":flag_sy:",
"category": "flags",
- "aliases": [":sy:"],
+ "aliases": [
+ ":sy:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sy"]
+ "keywords": [
+ "country",
+ "nation",
+ "sy"
+ ]
},
"flag_sz": {
"unicode": "1F1F8-1F1FF",
@@ -5922,9 +11845,39 @@
"name": "swaziland",
"shortname": ":flag_sz:",
"category": "flags",
- "aliases": [":sz:"],
+ "aliases": [
+ ":sz:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "sz"
+ ]
+ },
+ "flag_ta": {
+ "unicode": "1F1F9-1F1E6",
+ "unicode_alternates": "",
+ "name": "tristan da cunha",
+ "shortname": ":flag_ta:",
+ "category": "flags",
+ "aliases": [
+ ":ta:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "sz"]
+ "keywords": []
+ },
+ "flag_tc": {
+ "unicode": "1F1F9-1F1E8",
+ "unicode_alternates": "",
+ "name": "turks and caicos islands",
+ "shortname": ":flag_tc:",
+ "category": "flags",
+ "aliases": [
+ ":tc:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
},
"flag_td": {
"unicode": "1F1F9-1F1E9",
@@ -5932,9 +11885,28 @@
"name": "chad",
"shortname": ":flag_td:",
"category": "flags",
- "aliases": [":td:"],
+ "aliases": [
+ ":td:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "tchad",
+ "td"
+ ]
+ },
+ "flag_tf": {
+ "unicode": "1F1F9-1F1EB",
+ "unicode_alternates": "",
+ "name": "french southern territories",
+ "shortname": ":flag_tf:",
+ "category": "flags",
+ "aliases": [
+ ":tf:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "tchad", "td"]
+ "keywords": []
},
"flag_tg": {
"unicode": "1F1F9-1F1EC",
@@ -5942,9 +11914,16 @@
"name": "togo",
"shortname": ":flag_tg:",
"category": "flags",
- "aliases": [":tg:"],
+ "aliases": [
+ ":tg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "republique togolaise", "tg"]
+ "keywords": [
+ "country",
+ "nation",
+ "republique togolaise",
+ "tg"
+ ]
},
"flag_th": {
"unicode": "1F1F9-1F1ED",
@@ -5952,9 +11931,16 @@
"name": "thailand",
"shortname": ":flag_th:",
"category": "flags",
- "aliases": [":th:"],
+ "aliases": [
+ ":th:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "prathet thai", "th"]
+ "keywords": [
+ "country",
+ "nation",
+ "prathet thai",
+ "th"
+ ]
},
"flag_tj": {
"unicode": "1F1F9-1F1EF",
@@ -5962,9 +11948,28 @@
"name": "tajikistan",
"shortname": ":flag_tj:",
"category": "flags",
- "aliases": [":tj:"],
+ "aliases": [
+ ":tj:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "jumhurii tojikiston",
+ "tj"
+ ]
+ },
+ "flag_tk": {
+ "unicode": "1F1F9-1F1F0",
+ "unicode_alternates": "",
+ "name": "tokelau",
+ "shortname": ":flag_tk:",
+ "category": "flags",
+ "aliases": [
+ ":tk:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "jumhurii tojikiston", "tj"]
+ "keywords": []
},
"flag_tl": {
"unicode": "1F1F9-1F1F1",
@@ -5972,9 +11977,15 @@
"name": "east timor",
"shortname": ":flag_tl:",
"category": "flags",
- "aliases": [":tl:"],
+ "aliases": [
+ ":tl:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "tl"]
+ "keywords": [
+ "country",
+ "nation",
+ "tl"
+ ]
},
"flag_tm": {
"unicode": "1F1F9-1F1F2",
@@ -5982,9 +11993,15 @@
"name": "turkmenistan",
"shortname": ":flag_tm:",
"category": "flags",
- "aliases": [":turkmenistan:"],
+ "aliases": [
+ ":turkmenistan:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "tm"]
+ "keywords": [
+ "country",
+ "nation",
+ "tm"
+ ]
},
"flag_tn": {
"unicode": "1F1F9-1F1F3",
@@ -5992,9 +12009,16 @@
"name": "tunisia",
"shortname": ":flag_tn:",
"category": "flags",
- "aliases": [":tn:"],
+ "aliases": [
+ ":tn:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "tunis", "tn"]
+ "keywords": [
+ "country",
+ "nation",
+ "tunis",
+ "tn"
+ ]
},
"flag_to": {
"unicode": "1F1F9-1F1F4",
@@ -6002,9 +12026,15 @@
"name": "tonga",
"shortname": ":flag_to:",
"category": "flags",
- "aliases": [":to:"],
+ "aliases": [
+ ":to:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "to"]
+ "keywords": [
+ "country",
+ "nation",
+ "to"
+ ]
},
"flag_tr": {
"unicode": "1F1F9-1F1F7",
@@ -6012,9 +12042,15 @@
"name": "turkey",
"shortname": ":flag_tr:",
"category": "flags",
- "aliases": [":tr:"],
+ "aliases": [
+ ":tr:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "turkiye"]
+ "keywords": [
+ "country",
+ "nation",
+ "turkiye"
+ ]
},
"flag_tt": {
"unicode": "1F1F9-1F1F9",
@@ -6022,9 +12058,15 @@
"name": "trinidad and tobago",
"shortname": ":flag_tt:",
"category": "flags",
- "aliases": [":tt:"],
+ "aliases": [
+ ":tt:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "tt"]
+ "keywords": [
+ "country",
+ "nation",
+ "tt"
+ ]
},
"flag_tv": {
"unicode": "1F1F9-1F1FB",
@@ -6032,9 +12074,15 @@
"name": "tuvalu",
"shortname": ":flag_tv:",
"category": "flags",
- "aliases": [":tuvalu:"],
+ "aliases": [
+ ":tuvalu:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "tv"]
+ "keywords": [
+ "country",
+ "nation",
+ "tv"
+ ]
},
"flag_tw": {
"unicode": "1F1F9-1F1FC",
@@ -6042,9 +12090,16 @@
"name": "the republic of china",
"shortname": ":flag_tw:",
"category": "flags",
- "aliases": [":tw:"],
+ "aliases": [
+ ":tw:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "taiwan", "tw"]
+ "keywords": [
+ "country",
+ "nation",
+ "taiwan",
+ "tw"
+ ]
},
"flag_tz": {
"unicode": "1F1F9-1F1FF",
@@ -6052,9 +12107,15 @@
"name": "tanzania",
"shortname": ":flag_tz:",
"category": "flags",
- "aliases": [":tz:"],
+ "aliases": [
+ ":tz:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "tz"]
+ "keywords": [
+ "country",
+ "nation",
+ "tz"
+ ]
},
"flag_ua": {
"unicode": "1F1FA-1F1E6",
@@ -6062,9 +12123,16 @@
"name": "ukraine",
"shortname": ":flag_ua:",
"category": "flags",
- "aliases": [":ua:"],
+ "aliases": [
+ ":ua:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ukrayina", "ua"]
+ "keywords": [
+ "country",
+ "nation",
+ "ukrayina",
+ "ua"
+ ]
},
"flag_ug": {
"unicode": "1F1FA-1F1EC",
@@ -6072,9 +12140,27 @@
"name": "uganda",
"shortname": ":flag_ug:",
"category": "flags",
- "aliases": [":ug:"],
+ "aliases": [
+ ":ug:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ug"
+ ]
+ },
+ "flag_um": {
+ "unicode": "1F1FA-1F1F2",
+ "unicode_alternates": "",
+ "name": "united states minor outlying islands",
+ "shortname": ":flag_um:",
+ "category": "flags",
+ "aliases": [
+ ":um:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ug"]
+ "keywords": []
},
"flag_us": {
"unicode": "1F1FA-1F1F8",
@@ -6082,9 +12168,20 @@
"name": "united states",
"shortname": ":flag_us:",
"category": "flags",
- "aliases": [":us:"],
- "aliases_ascii": [],
- "keywords": ["american", "country", "nation", "usa", "united states of america", "america", "old glory", "us"]
+ "aliases": [
+ ":us:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "american",
+ "country",
+ "nation",
+ "usa",
+ "united states of america",
+ "america",
+ "old glory",
+ "us"
+ ]
},
"flag_uy": {
"unicode": "1F1FA-1F1FE",
@@ -6092,9 +12189,15 @@
"name": "uruguay",
"shortname": ":flag_uy:",
"category": "flags",
- "aliases": [":uy:"],
+ "aliases": [
+ ":uy:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "uy"]
+ "keywords": [
+ "country",
+ "nation",
+ "uy"
+ ]
},
"flag_uz": {
"unicode": "1F1FA-1F1FF",
@@ -6102,9 +12205,16 @@
"name": "uzbekistan",
"shortname": ":flag_uz:",
"category": "flags",
- "aliases": [":uz:"],
+ "aliases": [
+ ":uz:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "uzbekiston respublikasi", "uz"]
+ "keywords": [
+ "country",
+ "nation",
+ "uzbekiston respublikasi",
+ "uz"
+ ]
},
"flag_va": {
"unicode": "1F1FB-1F1E6",
@@ -6112,9 +12222,15 @@
"name": "the vatican city",
"shortname": ":flag_va:",
"category": "flags",
- "aliases": [":va:"],
+ "aliases": [
+ ":va:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "va"]
+ "keywords": [
+ "country",
+ "nation",
+ "va"
+ ]
},
"flag_vc": {
"unicode": "1F1FB-1F1E8",
@@ -6122,9 +12238,15 @@
"name": "saint vincent and the grenadines",
"shortname": ":flag_vc:",
"category": "flags",
- "aliases": [":vc:"],
+ "aliases": [
+ ":vc:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "vc"]
+ "keywords": [
+ "country",
+ "nation",
+ "vc"
+ ]
},
"flag_ve": {
"unicode": "1F1FB-1F1EA",
@@ -6132,9 +12254,27 @@
"name": "venezuela",
"shortname": ":flag_ve:",
"category": "flags",
- "aliases": [":ve:"],
+ "aliases": [
+ ":ve:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "ve"
+ ]
+ },
+ "flag_vg": {
+ "unicode": "1F1FB-1F1EC",
+ "unicode_alternates": "",
+ "name": "british virgin islands",
+ "shortname": ":flag_vg:",
+ "category": "flags",
+ "aliases": [
+ ":vg:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "ve"]
+ "keywords": []
},
"flag_vi": {
"unicode": "1F1FB-1F1EE",
@@ -6142,9 +12282,15 @@
"name": "u.s. virgin islands",
"shortname": ":flag_vi:",
"category": "flags",
- "aliases": [":vi:"],
+ "aliases": [
+ ":vi:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "vi"]
+ "keywords": [
+ "country",
+ "nation",
+ "vi"
+ ]
},
"flag_vn": {
"unicode": "1F1FB-1F1F3",
@@ -6152,9 +12298,16 @@
"name": "vietnam",
"shortname": ":flag_vn:",
"category": "flags",
- "aliases": [":vn:"],
+ "aliases": [
+ ":vn:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "viet nam", "vn"]
+ "keywords": [
+ "country",
+ "nation",
+ "viet nam",
+ "vn"
+ ]
},
"flag_vu": {
"unicode": "1F1FB-1F1FA",
@@ -6162,9 +12315,15 @@
"name": "vanuatu",
"shortname": ":flag_vu:",
"category": "flags",
- "aliases": [":vu:"],
+ "aliases": [
+ ":vu:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "vu"]
+ "keywords": [
+ "country",
+ "nation",
+ "vu"
+ ]
},
"flag_wf": {
"unicode": "1F1FC-1F1EB",
@@ -6172,9 +12331,15 @@
"name": "wallis and futuna",
"shortname": ":flag_wf:",
"category": "flags",
- "aliases": [":wf:"],
+ "aliases": [
+ ":wf:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "wf"]
+ "keywords": [
+ "country",
+ "nation",
+ "wf"
+ ]
},
"flag_white": {
"unicode": "1F3F3",
@@ -6182,9 +12347,14 @@
"name": "waving white flag",
"shortname": ":flag_white:",
"category": "objects_symbols",
- "aliases": [":waving_white_flag:"],
+ "aliases": [
+ ":waving_white_flag:"
+ ],
"aliases_ascii": [],
- "keywords": ["symbol", "signal"]
+ "keywords": [
+ "symbol",
+ "signal"
+ ]
},
"flag_ws": {
"unicode": "1F1FC-1F1F8",
@@ -6192,9 +12362,16 @@
"name": "samoa",
"shortname": ":flag_ws:",
"category": "flags",
- "aliases": [":ws:"],
+ "aliases": [
+ ":ws:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "american samoa", "ws"]
+ "keywords": [
+ "country",
+ "nation",
+ "american samoa",
+ "ws"
+ ]
},
"flag_xk": {
"unicode": "1F1FD-1F1F0",
@@ -6202,9 +12379,15 @@
"name": "kosovo",
"shortname": ":flag_xk:",
"category": "flags",
- "aliases": [":xk:"],
+ "aliases": [
+ ":xk:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "xk"]
+ "keywords": [
+ "country",
+ "nation",
+ "xk"
+ ]
},
"flag_ye": {
"unicode": "1F1FE-1F1EA",
@@ -6212,9 +12395,28 @@
"name": "yemen",
"shortname": ":flag_ye:",
"category": "flags",
- "aliases": [":ye:"],
+ "aliases": [
+ ":ye:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "country",
+ "nation",
+ "al yaman",
+ "ye"
+ ]
+ },
+ "flag_yt": {
+ "unicode": "1F1FE-1F1F9",
+ "unicode_alternates": "",
+ "name": "mayotte",
+ "shortname": ":flag_yt:",
+ "category": "flags",
+ "aliases": [
+ ":yt:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "al yaman", "ye"]
+ "keywords": []
},
"flag_za": {
"unicode": "1F1FF-1F1E6",
@@ -6222,9 +12424,14 @@
"name": "south africa",
"shortname": ":flag_za:",
"category": "flags",
- "aliases": [":za:"],
+ "aliases": [
+ ":za:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation"]
+ "keywords": [
+ "country",
+ "nation"
+ ]
},
"flag_zm": {
"unicode": "1F1FF-1F1F2",
@@ -6232,9 +12439,15 @@
"name": "zambia",
"shortname": ":flag_zm:",
"category": "flags",
- "aliases": [":zm:"],
+ "aliases": [
+ ":zm:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "zm"]
+ "keywords": [
+ "country",
+ "nation",
+ "zm"
+ ]
},
"flag_zw": {
"unicode": "1F1FF-1F1FC",
@@ -6242,9 +12455,15 @@
"name": "zimbabwe",
"shortname": ":flag_zw:",
"category": "flags",
- "aliases": [":zw:"],
+ "aliases": [
+ ":zw:"
+ ],
"aliases_ascii": [],
- "keywords": ["country", "nation", "zw"]
+ "keywords": [
+ "country",
+ "nation",
+ "zw"
+ ]
},
"flags": {
"unicode": "1F38F",
@@ -6254,7 +12473,23 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["banner", "carp", "fish", "japanese", "koinobori", "children", "kids", "boys", "celebration", "happiness", "carp", "streamers", "japanese", "holiday", "flags"],
+ "keywords": [
+ "banner",
+ "carp",
+ "fish",
+ "japanese",
+ "koinobori",
+ "children",
+ "kids",
+ "boys",
+ "celebration",
+ "happiness",
+ "carp",
+ "streamers",
+ "japanese",
+ "holiday",
+ "flags"
+ ],
"moji": "🎏"
},
"flashlight": {
@@ -6265,18 +12500,36 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dark"],
+ "keywords": [
+ "dark"
+ ],
"moji": "🔦"
},
+ "fleur-de-lis": {
+ "unicode": "269C",
+ "unicode_alternates": "",
+ "name": "fleur-de-lis",
+ "shortname": ":fleur-de-lis:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "symbol"
+ ]
+ },
"flip_phone": {
"unicode": "1F581",
"unicode_alternates": [],
"name": "clamshell mobile phone",
"shortname": ":flip_phone:",
"category": "objects_symbols",
- "aliases": [":clamshell_mobile_phone:"],
+ "aliases": [
+ ":clamshell_mobile_phone:"
+ ],
"aliases_ascii": [],
- "keywords": ["cellphone"]
+ "keywords": [
+ "cellphone"
+ ]
},
"floppy_black": {
"unicode": "1F5AA",
@@ -6284,9 +12537,20 @@
"name": "black hard shell floppy disk",
"shortname": ":floppy_black:",
"category": "objects_symbols",
- "aliases": [":black_hard_shell_floppy_disk:"],
- "aliases_ascii": [],
- "keywords": ["oldschool", "save", "technology", "storage", "information", "computer", "drive", "megabyte"]
+ "aliases": [
+ ":black_hard_shell_floppy_disk:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "oldschool",
+ "save",
+ "technology",
+ "storage",
+ "information",
+ "computer",
+ "drive",
+ "megabyte"
+ ]
},
"floppy_disk": {
"unicode": "1F4BE",
@@ -6296,7 +12560,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["oldschool", "save", "technology", "floppy", "disk", "storage", "information", "computer", "drive", "megabyte"],
+ "keywords": [
+ "oldschool",
+ "save",
+ "technology",
+ "floppy",
+ "disk",
+ "storage",
+ "information",
+ "computer",
+ "drive",
+ "megabyte"
+ ],
"moji": "💾"
},
"floppy_white": {
@@ -6305,9 +12580,20 @@
"name": "white hard shell floppy disk",
"shortname": ":floppy_white:",
"category": "objects_symbols",
- "aliases": [":white_hard_shell_floppy_disk:"],
- "aliases_ascii": [],
- "keywords": ["oldschool", "save", "technology", "storage", "information", "computer", "drive", "megabyte"]
+ "aliases": [
+ ":white_hard_shell_floppy_disk:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "oldschool",
+ "save",
+ "technology",
+ "storage",
+ "information",
+ "computer",
+ "drive",
+ "megabyte"
+ ]
},
"flower_playing_cards": {
"unicode": "1F3B4",
@@ -6317,7 +12603,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["playing", "card", "flower", "game", "august", "moon", "special"],
+ "keywords": [
+ "playing",
+ "card",
+ "flower",
+ "game",
+ "august",
+ "moon",
+ "special"
+ ],
"moji": "🎴"
},
"flushed": {
@@ -6327,8 +12621,21 @@
"shortname": ":flushed:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":$", "=$"],
- "keywords": ["blush", "face", "flattered", "flush", "blush", "red", "pink", "cheeks", "shy"],
+ "aliases_ascii": [
+ ":$",
+ "=$"
+ ],
+ "keywords": [
+ "blush",
+ "face",
+ "flattered",
+ "flush",
+ "blush",
+ "red",
+ "pink",
+ "cheeks",
+ "shy"
+ ],
"moji": "😳"
},
"fog": {
@@ -6339,7 +12646,12 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["weather", "damp", "cloud", "hazy"]
+ "keywords": [
+ "weather",
+ "damp",
+ "cloud",
+ "hazy"
+ ]
},
"foggy": {
"unicode": "1F301",
@@ -6349,7 +12661,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mountain", "photo", "bridge", "weather", "fog", "foggy"],
+ "keywords": [
+ "mountain",
+ "photo",
+ "bridge",
+ "weather",
+ "fog",
+ "foggy"
+ ],
"moji": "🌁"
},
"folder": {
@@ -6360,7 +12679,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents"]
+ "keywords": [
+ "documents"
+ ]
},
"folder_open": {
"unicode": "1F5C1",
@@ -6368,9 +12689,14 @@
"name": "open folder",
"shortname": ":folder_open:",
"category": "objects_symbols",
- "aliases": [":open_folder:"],
+ "aliases": [
+ ":open_folder:"
+ ],
"aliases_ascii": [],
- "keywords": ["documents", "load"]
+ "keywords": [
+ "documents",
+ "load"
+ ]
},
"football": {
"unicode": "1F3C8",
@@ -6380,7 +12706,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["NFL", "balls", "sports", "football", "ball", "sport", "america", "american"],
+ "keywords": [
+ "NFL",
+ "balls",
+ "sports",
+ "football",
+ "ball",
+ "sport",
+ "america",
+ "american"
+ ],
"moji": "🏈"
},
"footprints": {
@@ -6391,7 +12726,9 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["feet"],
+ "keywords": [
+ "feet"
+ ],
"moji": "👣"
},
"fork_and_knife": {
@@ -6402,7 +12739,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cutlery", "kitchen", "fork", "knife", "restaurant", "meal", "food", "eat"],
+ "keywords": [
+ "cutlery",
+ "kitchen",
+ "fork",
+ "knife",
+ "restaurant",
+ "meal",
+ "food",
+ "eat"
+ ],
"moji": "🍴"
},
"fork_knife_plate": {
@@ -6411,31 +12757,51 @@
"name": "fork and knife with plate",
"shortname": ":fork_knife_plate:",
"category": "travel_places",
- "aliases": [":fork_and_knife_with_plate:"],
- "aliases_ascii": [],
- "keywords": ["meal", "food", "breakfast", "lunch", "dinner", "utensils", "setting"]
+ "aliases": [
+ ":fork_and_knife_with_plate:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "meal",
+ "food",
+ "breakfast",
+ "lunch",
+ "dinner",
+ "utensils",
+ "setting"
+ ]
},
"fountain": {
"unicode": "26F2",
- "unicode_alternates": ["26F2-FE0F"],
+ "unicode_alternates": [
+ "26F2-FE0F"
+ ],
"name": "fountain",
"shortname": ":fountain:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["photo"],
+ "keywords": [
+ "photo"
+ ],
"moji": "⛲"
},
"four": {
"moji": "4️⃣",
"unicode": "0034-20E3",
- "unicode_alternates": ["0034-FE0F-20E3"],
+ "unicode_alternates": [
+ "0034-FE0F-20E3"
+ ],
"name": "digit four",
"shortname": ":four:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["4", "blue-square", "numbers"]
+ "keywords": [
+ "4",
+ "blue-square",
+ "numbers"
+ ]
},
"four_leaf_clover": {
"unicode": "1F340",
@@ -6445,7 +12811,20 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["lucky", "nature", "plant", "vegetable", "clover", "four", "leaf", "luck", "irish", "saint", "patrick", "green"],
+ "keywords": [
+ "lucky",
+ "nature",
+ "plant",
+ "vegetable",
+ "clover",
+ "four",
+ "leaf",
+ "luck",
+ "irish",
+ "saint",
+ "patrick",
+ "green"
+ ],
"moji": "🍀"
},
"frame_photo": {
@@ -6454,9 +12833,13 @@
"name": "frame with picture",
"shortname": ":frame_photo:",
"category": "objects_symbols",
- "aliases": [":frame_with_picture:"],
+ "aliases": [
+ ":frame_with_picture:"
+ ],
"aliases_ascii": [],
- "keywords": ["photo"]
+ "keywords": [
+ "photo"
+ ]
},
"frame_tiles": {
"unicode": "1F5BD",
@@ -6464,9 +12847,14 @@
"name": "frame with tiles",
"shortname": ":frame_tiles:",
"category": "objects_symbols",
- "aliases": [":frame_with_tiles:"],
+ "aliases": [
+ ":frame_with_tiles:"
+ ],
"aliases_ascii": [],
- "keywords": ["photo", "painting"]
+ "keywords": [
+ "photo",
+ "painting"
+ ]
},
"frame_x": {
"unicode": "1F5BE",
@@ -6474,9 +12862,14 @@
"name": "frame with an x",
"shortname": ":frame_x:",
"category": "objects_symbols",
- "aliases": [":frame_with_an_x:"],
+ "aliases": [
+ ":frame_with_an_x:"
+ ],
"aliases_ascii": [],
- "keywords": ["photo", "painting"]
+ "keywords": [
+ "photo",
+ "painting"
+ ]
},
"free": {
"unicode": "1F193",
@@ -6486,7 +12879,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "words"],
+ "keywords": [
+ "blue-square",
+ "words"
+ ],
"moji": "🆓"
},
"fried_shrimp": {
@@ -6497,7 +12893,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "food", "shrimp", "fried", "seafood", "small", "fish"],
+ "keywords": [
+ "animal",
+ "food",
+ "shrimp",
+ "fried",
+ "seafood",
+ "small",
+ "fish"
+ ],
"moji": "🍤"
},
"fries": {
@@ -6508,7 +12912,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chips", "food", "fries", "french", "potato", "fry", "russet", "idaho"],
+ "keywords": [
+ "chips",
+ "food",
+ "fries",
+ "french",
+ "potato",
+ "fry",
+ "russet",
+ "idaho"
+ ],
"moji": "🍟"
},
"frog": {
@@ -6519,7 +12932,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐸"
},
"frowning": {
@@ -6528,20 +12944,50 @@
"name": "frowning face with open mouth",
"shortname": ":frowning:",
"category": "emoticons",
- "aliases": [":anguished:"],
- "aliases_ascii": [],
- "keywords": ["aw", "face", "frown", "sad", "pout", "sulk", "glower"],
+ "aliases": [
+ ":anguished:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "aw",
+ "face",
+ "frown",
+ "sad",
+ "pout",
+ "sulk",
+ "glower"
+ ],
"moji": "😦"
},
+ "frowning2": {
+ "unicode": "2639",
+ "unicode_alternates": "",
+ "name": "white frowning face",
+ "shortname": ":frowning2:",
+ "category": "people",
+ "aliases": [
+ ":white_frowning_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "frown",
+ "person"
+ ]
+ },
"fuelpump": {
"unicode": "26FD",
- "unicode_alternates": ["26FD-FE0F"],
+ "unicode_alternates": [
+ "26FD-FE0F"
+ ],
"name": "fuel pump",
"shortname": ":fuelpump:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["gas station", "petroleum"],
+ "keywords": [
+ "gas station",
+ "petroleum"
+ ],
"moji": "⛽"
},
"full_moon": {
@@ -6552,7 +12998,20 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "yellow", "moon", "full", "sky", "night", "cheese", "phase", "monster", "spooky", "werewolves", "twilight"],
+ "keywords": [
+ "nature",
+ "yellow",
+ "moon",
+ "full",
+ "sky",
+ "night",
+ "cheese",
+ "phase",
+ "monster",
+ "spooky",
+ "werewolves",
+ "twilight"
+ ],
"moji": "🌕"
},
"full_moon_with_face": {
@@ -6563,7 +13022,20 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["night", "moon", "full", "anthropomorphic", "face", "sky", "night", "cheese", "phase", "spooky", "werewolves", "monsters"],
+ "keywords": [
+ "night",
+ "moon",
+ "full",
+ "anthropomorphic",
+ "face",
+ "sky",
+ "night",
+ "cheese",
+ "phase",
+ "spooky",
+ "werewolves",
+ "monsters"
+ ],
"moji": "🌝"
},
"game_die": {
@@ -6574,9 +13046,30 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dice", "game", "die", "dice", "craps", "gamble", "play"],
+ "keywords": [
+ "dice",
+ "game",
+ "die",
+ "dice",
+ "craps",
+ "gamble",
+ "play"
+ ],
"moji": "🎲"
},
+ "gear": {
+ "unicode": "2699",
+ "unicode_alternates": "",
+ "name": "gear",
+ "shortname": ":gear:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "object",
+ "tool"
+ ]
+ },
"gem": {
"unicode": "1F48E",
"unicode_alternates": [],
@@ -6585,18 +13078,35 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue", "ruby"],
+ "keywords": [
+ "blue",
+ "ruby"
+ ],
"moji": "💎"
},
"gemini": {
"unicode": "264A",
- "unicode_alternates": ["264A-FE0F"],
+ "unicode_alternates": [
+ "264A-FE0F"
+ ],
"name": "gemini",
"shortname": ":gemini:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["gemini", "twins", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "gemini",
+ "twins",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♊"
},
"ghost": {
@@ -6607,7 +13117,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["halloween"],
+ "keywords": [
+ "halloween"
+ ],
"moji": "👻"
},
"gift": {
@@ -6618,7 +13130,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["birthday", "christmas", "present", "xmas", "gift", "present", "wrap", "package", "birthday", "wedding"],
+ "keywords": [
+ "birthday",
+ "christmas",
+ "present",
+ "xmas",
+ "gift",
+ "present",
+ "wrap",
+ "package",
+ "birthday",
+ "wedding"
+ ],
"moji": "🎁"
},
"gift_heart": {
@@ -6629,7 +13152,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["love", "valentines"],
+ "keywords": [
+ "love",
+ "valentines"
+ ],
"moji": "💝"
},
"girl": {
@@ -6640,9 +13166,82 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "woman"],
+ "keywords": [
+ "female",
+ "woman"
+ ],
"moji": "👧"
},
+ "girl_tone1": {
+ "unicode": "1F467-1F3FB",
+ "unicode_alternates": "",
+ "name": "girl tone 1",
+ "shortname": ":girl_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "kid",
+ "child"
+ ]
+ },
+ "girl_tone2": {
+ "unicode": "1F467-1F3FC",
+ "unicode_alternates": "",
+ "name": "girl tone 2",
+ "shortname": ":girl_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "kid",
+ "child"
+ ]
+ },
+ "girl_tone3": {
+ "unicode": "1F467-1F3FD",
+ "unicode_alternates": "",
+ "name": "girl tone 3",
+ "shortname": ":girl_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "kid",
+ "child"
+ ]
+ },
+ "girl_tone4": {
+ "unicode": "1F467-1F3FE",
+ "unicode_alternates": "",
+ "name": "girl tone 4",
+ "shortname": ":girl_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "kid",
+ "child"
+ ]
+ },
+ "girl_tone5": {
+ "unicode": "1F467-1F3FF",
+ "unicode_alternates": "",
+ "name": "girl tone 5",
+ "shortname": ":girl_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "kid",
+ "child"
+ ]
+ },
"girls_symbol": {
"unicode": "1F6CA",
"unicode_alternates": [],
@@ -6651,7 +13250,10 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "child"]
+ "keywords": [
+ "female",
+ "child"
+ ]
},
"globe_with_meridians": {
"unicode": "1F310",
@@ -6661,7 +13263,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["earth", "international", "world", "earth", "meridian", "globe", "space", "planet", "home"],
+ "keywords": [
+ "earth",
+ "international",
+ "world",
+ "earth",
+ "meridian",
+ "globe",
+ "space",
+ "planet",
+ "home"
+ ],
"moji": "🌐"
},
"goat": {
@@ -6672,18 +13284,31 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "goat", "sheep", "kid", "billy", "livestock"],
+ "keywords": [
+ "animal",
+ "nature",
+ "goat",
+ "sheep",
+ "kid",
+ "billy",
+ "livestock"
+ ],
"moji": "🐐"
},
"golf": {
"unicode": "26F3",
- "unicode_alternates": ["26F3-FE0F"],
+ "unicode_alternates": [
+ "26F3-FE0F"
+ ],
"name": "flag in hole",
"shortname": ":golf:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["business", "sports"],
+ "keywords": [
+ "business",
+ "sports"
+ ],
"moji": "⛳"
},
"golfer": {
@@ -6694,7 +13319,13 @@
"category": "activity",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sport", "par", "birdie", "eagle", "mulligan"]
+ "keywords": [
+ "sport",
+ "par",
+ "birdie",
+ "eagle",
+ "mulligan"
+ ]
},
"grapes": {
"unicode": "1F347",
@@ -6704,7 +13335,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "grapes", "wine", "vinegar", "fruit", "cluster", "vine"],
+ "keywords": [
+ "food",
+ "fruit",
+ "grapes",
+ "wine",
+ "vinegar",
+ "fruit",
+ "cluster",
+ "vine"
+ ],
"moji": "🍇"
},
"green_apple": {
@@ -6715,7 +13355,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fruit", "nature", "apple", "fruit", "green", "pie", "granny", "smith", "core"],
+ "keywords": [
+ "fruit",
+ "nature",
+ "apple",
+ "fruit",
+ "green",
+ "pie",
+ "granny",
+ "smith",
+ "core"
+ ],
"moji": "🍏"
},
"green_book": {
@@ -6726,7 +13376,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["knowledge", "library", "read"],
+ "keywords": [
+ "knowledge",
+ "library",
+ "read"
+ ],
"moji": "📗"
},
"green_heart": {
@@ -6737,7 +13391,22 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines", "green", "heart", "love", "nature", "rebirth", "reborn", "jealous", "clingy", "envious", "possessive"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines",
+ "green",
+ "heart",
+ "love",
+ "nature",
+ "rebirth",
+ "reborn",
+ "jealous",
+ "clingy",
+ "envious",
+ "possessive"
+ ],
"moji": "💚"
},
"grey_exclamation": {
@@ -6748,7 +13417,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["surprise"],
+ "keywords": [
+ "surprise"
+ ],
"moji": "❕"
},
"grey_question": {
@@ -6759,7 +13430,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["doubts"],
+ "keywords": [
+ "doubts"
+ ],
"moji": "❔"
},
"grimacing": {
@@ -6770,7 +13443,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "grimace", "teeth", "grimace", "disapprove", "pain"],
+ "keywords": [
+ "face",
+ "grimace",
+ "teeth",
+ "grimace",
+ "disapprove",
+ "pain"
+ ],
"moji": "😬"
},
"grin": {
@@ -6781,7 +13461,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "happy", "joy", "smile", "grin", "grinning", "smiling", "smile", "smiley"],
+ "keywords": [
+ "face",
+ "happy",
+ "joy",
+ "smile",
+ "grin",
+ "grinning",
+ "smiling",
+ "smile",
+ "smiley"
+ ],
"moji": "😁"
},
"grinning": {
@@ -6792,7 +13482,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "happy", "joy", "smile", "grin", "grinning", "smiling", "smile", "smiley"],
+ "keywords": [
+ "face",
+ "happy",
+ "joy",
+ "smile",
+ "grin",
+ "grinning",
+ "smiling",
+ "smile",
+ "smiley"
+ ],
"moji": "🕧"
},
"guardsman": {
@@ -6803,9 +13503,138 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["british", "gb", "male", "man", "uk", "guardsman", "guard", "bearskin", "hat", "british", "queen", "ceremonial", "military"],
+ "keywords": [
+ "british",
+ "gb",
+ "male",
+ "man",
+ "uk",
+ "guardsman",
+ "guard",
+ "bearskin",
+ "hat",
+ "british",
+ "queen",
+ "ceremonial",
+ "military"
+ ],
"moji": "💂"
},
+ "guardsman_tone1": {
+ "unicode": "1F482-1F3FB",
+ "unicode_alternates": "",
+ "name": "guardsman tone 1",
+ "shortname": ":guardsman_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "british",
+ "gb",
+ "male",
+ "man",
+ "uk",
+ "guard",
+ "bearskin",
+ "hat",
+ "british",
+ "queen",
+ "ceremonial",
+ "military"
+ ]
+ },
+ "guardsman_tone2": {
+ "unicode": "1F482-1F3FC",
+ "unicode_alternates": "",
+ "name": "guardsman tone 2",
+ "shortname": ":guardsman_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "british",
+ "gb",
+ "male",
+ "man",
+ "uk",
+ "guard",
+ "bearskin",
+ "hat",
+ "british",
+ "queen",
+ "ceremonial",
+ "military"
+ ]
+ },
+ "guardsman_tone3": {
+ "unicode": "1F482-1F3FD",
+ "unicode_alternates": "",
+ "name": "guardsman tone 3",
+ "shortname": ":guardsman_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "british",
+ "gb",
+ "male",
+ "man",
+ "uk",
+ "guard",
+ "bearskin",
+ "hat",
+ "british",
+ "queen",
+ "ceremonial",
+ "military"
+ ]
+ },
+ "guardsman_tone4": {
+ "unicode": "1F482-1F3FE",
+ "unicode_alternates": "",
+ "name": "guardsman tone 4",
+ "shortname": ":guardsman_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "british",
+ "gb",
+ "male",
+ "man",
+ "uk",
+ "guard",
+ "bearskin",
+ "hat",
+ "british",
+ "queen",
+ "ceremonial",
+ "military"
+ ]
+ },
+ "guardsman_tone5": {
+ "unicode": "1F482-1F3FF",
+ "unicode_alternates": "",
+ "name": "guardsman tone 5",
+ "shortname": ":guardsman_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "british",
+ "gb",
+ "male",
+ "man",
+ "uk",
+ "guard",
+ "bearskin",
+ "hat",
+ "british",
+ "queen",
+ "ceremonial",
+ "military"
+ ]
+ },
"guitar": {
"unicode": "1F3B8",
"unicode_alternates": [],
@@ -6814,7 +13643,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["instrument", "music", "guitar", "string", "music", "instrument", "jam", "rock", "acoustic", "electric"],
+ "keywords": [
+ "instrument",
+ "music",
+ "guitar",
+ "string",
+ "music",
+ "instrument",
+ "jam",
+ "rock",
+ "acoustic",
+ "electric"
+ ],
"moji": "🎸"
},
"gun": {
@@ -6825,7 +13665,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["violence", "weapon"],
+ "keywords": [
+ "violence",
+ "weapon"
+ ],
"moji": "🔫"
},
"haircut": {
@@ -6836,9 +13679,83 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girl", "woman"],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ],
"moji": "💇"
},
+ "haircut_tone1": {
+ "unicode": "1F487-1F3FB",
+ "unicode_alternates": "",
+ "name": "haircut tone 1",
+ "shortname": ":haircut_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "haircut_tone2": {
+ "unicode": "1F487-1F3FC",
+ "unicode_alternates": "",
+ "name": "haircut tone 2",
+ "shortname": ":haircut_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "haircut_tone3": {
+ "unicode": "1F487-1F3FD",
+ "unicode_alternates": "",
+ "name": "haircut tone 3",
+ "shortname": ":haircut_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "haircut_tone4": {
+ "unicode": "1F487-1F3FE",
+ "unicode_alternates": "",
+ "name": "haircut tone 4",
+ "shortname": ":haircut_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "haircut_tone5": {
+ "unicode": "1F487-1F3FF",
+ "unicode_alternates": "",
+ "name": "haircut tone 5",
+ "shortname": ":haircut_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
"hamburger": {
"unicode": "1F354",
"unicode_alternates": [],
@@ -6847,7 +13764,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "meat", "hamburger", "burger", "meat", "cow", "beef"],
+ "keywords": [
+ "food",
+ "meat",
+ "hamburger",
+ "burger",
+ "meat",
+ "cow",
+ "beef"
+ ],
"moji": "🍔"
},
"hammer": {
@@ -6858,9 +13783,31 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["done", "judge", "law", "ruling", "tools", "verdict"],
+ "keywords": [
+ "done",
+ "judge",
+ "law",
+ "ruling",
+ "tools",
+ "verdict"
+ ],
"moji": "🔨"
},
+ "hammer_pick": {
+ "unicode": "2692",
+ "unicode_alternates": "",
+ "name": "hammer and pick",
+ "shortname": ":hammer_pick:",
+ "category": "objects",
+ "aliases": [
+ ":hammer_and_pick:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "object",
+ "tool"
+ ]
+ },
"hamster": {
"unicode": "1F439",
"unicode_alternates": [],
@@ -6869,7 +13816,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐹"
},
"hand_splayed": {
@@ -6878,9 +13828,16 @@
"name": "raised hand with fingers splayed",
"shortname": ":hand_splayed:",
"category": "people",
- "aliases": [":raised_hand_with_fingers_splayed:"],
+ "aliases": [
+ ":raised_hand_with_fingers_splayed:"
+ ],
"aliases_ascii": [],
- "keywords": ["hi", "five", "stop", "halt"]
+ "keywords": [
+ "hi",
+ "five",
+ "stop",
+ "halt"
+ ]
},
"hand_splayed_reverse": {
"unicode": "1F591",
@@ -6888,9 +13845,101 @@
"name": "reversed raised hand with fingers splayed",
"shortname": ":hand_splayed_reverse:",
"category": "people",
- "aliases": [":reversed_raised_hand_with_fingers_splayed:"],
+ "aliases": [
+ ":reversed_raised_hand_with_fingers_splayed:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hi",
+ "five",
+ "stop",
+ "halt"
+ ]
+ },
+ "hand_splayed_tone1": {
+ "unicode": "1F590-1F3FB",
+ "unicode_alternates": "",
+ "name": "raised hand with fingers splayed tone 1",
+ "shortname": ":hand_splayed_tone1:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_fingers_splayed_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hi",
+ "five",
+ "stop",
+ "halt"
+ ]
+ },
+ "hand_splayed_tone2": {
+ "unicode": "1F590-1F3FC",
+ "unicode_alternates": "",
+ "name": "raised hand with fingers splayed tone 2",
+ "shortname": ":hand_splayed_tone2:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_fingers_splayed_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hi",
+ "five",
+ "stop",
+ "halt"
+ ]
+ },
+ "hand_splayed_tone3": {
+ "unicode": "1F590-1F3FD",
+ "unicode_alternates": "",
+ "name": "raised hand with fingers splayed tone 3",
+ "shortname": ":hand_splayed_tone3:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_fingers_splayed_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hi",
+ "five",
+ "stop",
+ "halt"
+ ]
+ },
+ "hand_splayed_tone4": {
+ "unicode": "1F590-1F3FE",
+ "unicode_alternates": "",
+ "name": "raised hand with fingers splayed tone 4",
+ "shortname": ":hand_splayed_tone4:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_fingers_splayed_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hi",
+ "five",
+ "stop",
+ "halt"
+ ]
+ },
+ "hand_splayed_tone5": {
+ "unicode": "1F590-1F3FF",
+ "unicode_alternates": "",
+ "name": "raised hand with fingers splayed tone 5",
+ "shortname": ":hand_splayed_tone5:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_fingers_splayed_tone5:"
+ ],
"aliases_ascii": [],
- "keywords": ["hi", "five", "stop", "halt"]
+ "keywords": [
+ "hi",
+ "five",
+ "stop",
+ "halt"
+ ]
},
"hand_victory": {
"unicode": "1F594",
@@ -6898,9 +13947,13 @@
"name": "reversed victory hand",
"shortname": ":hand_victory:",
"category": "people",
- "aliases": [":reversed_victory_hand:"],
+ "aliases": [
+ ":reversed_victory_hand:"
+ ],
"aliases_ascii": [],
- "keywords": ["fu"]
+ "keywords": [
+ "fu"
+ ]
},
"handbag": {
"unicode": "1F45C",
@@ -6910,7 +13963,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["accessories", "accessory", "bag", "fashion"],
+ "keywords": [
+ "accessories",
+ "accessory",
+ "bag",
+ "fashion"
+ ],
"moji": "👜"
},
"hard_disk": {
@@ -6921,18 +13979,32 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["save", "technology", "storage", "information", "computer", "drive", "megabyte", "gigabyte", "hd"]
+ "keywords": [
+ "save",
+ "technology",
+ "storage",
+ "information",
+ "computer",
+ "drive",
+ "megabyte",
+ "gigabyte",
+ "hd"
+ ]
},
"hash": {
"moji": "#⃣",
"unicode": "0023-20E3",
- "unicode_alternates": ["0023-FE0F-20E3"],
+ "unicode_alternates": [
+ "0023-FE0F-20E3"
+ ],
"name": "number sign",
"shortname": ":hash:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["symbol"]
+ "keywords": [
+ "symbol"
+ ]
},
"hatched_chick": {
"unicode": "1F425",
@@ -6942,7 +14014,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["baby", "chicken", "chick", "baby", "bird", "chicken", "young", "woman", "cute"],
+ "keywords": [
+ "baby",
+ "chicken",
+ "chick",
+ "baby",
+ "bird",
+ "chicken",
+ "young",
+ "woman",
+ "cute"
+ ],
"moji": "🐥"
},
"hatching_chick": {
@@ -6953,9 +14035,33 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["born", "chicken", "egg", "chick", "egg", "baby", "bird", "chicken", "young", "woman", "cute"],
+ "keywords": [
+ "born",
+ "chicken",
+ "egg",
+ "chick",
+ "egg",
+ "baby",
+ "bird",
+ "chicken",
+ "young",
+ "woman",
+ "cute"
+ ],
"moji": "🐣"
},
+ "head_bandage": {
+ "unicode": "1F915",
+ "unicode_alternates": "",
+ "name": "face with head-bandage",
+ "shortname": ":head_bandage:",
+ "category": "people",
+ "aliases": [
+ ":face_with_head_bandage:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"headphones": {
"unicode": "1F3A7",
"unicode_alternates": [],
@@ -6964,7 +14070,19 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["gadgets", "music", "score", "headphone", "sound", "music", "ears", "beats", "buds", "audio", "listen"],
+ "keywords": [
+ "gadgets",
+ "music",
+ "score",
+ "headphone",
+ "sound",
+ "music",
+ "ears",
+ "beats",
+ "buds",
+ "audio",
+ "listen"
+ ],
"moji": "🎧"
},
"hear_no_evil": {
@@ -6975,19 +14093,47 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "monkey", "monkey", "ears", "hear", "sound", "kikazaru"],
+ "keywords": [
+ "animal",
+ "monkey",
+ "monkey",
+ "ears",
+ "hear",
+ "sound",
+ "kikazaru"
+ ],
"moji": "🙉"
},
"heart": {
"moji": "❤",
"unicode": "2764",
- "unicode_alternates": ["2764-FE0F"],
+ "unicode_alternates": [
+ "2764-FE0F"
+ ],
"name": "heavy black heart",
"shortname": ":heart:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["<3"],
- "keywords": ["like", "love", "red", "pink", "black", "heart", "love", "passion", "romance", "intense", "desire", "death", "evil", "cold", "valentines"]
+ "aliases_ascii": [
+ "<3"
+ ],
+ "keywords": [
+ "like",
+ "love",
+ "red",
+ "pink",
+ "black",
+ "heart",
+ "love",
+ "passion",
+ "romance",
+ "intense",
+ "desire",
+ "death",
+ "evil",
+ "cold",
+ "valentines"
+ ]
},
"heart_decoration": {
"unicode": "1F49F",
@@ -6997,9 +14143,29 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["like", "love", "purple-square"],
+ "keywords": [
+ "like",
+ "love",
+ "purple-square"
+ ],
"moji": "💟"
},
+ "heart_exclamation": {
+ "unicode": "2763",
+ "unicode_alternates": "",
+ "name": "heavy heart exclamation mark ornament",
+ "shortname": ":heart_exclamation:",
+ "category": "symbols",
+ "aliases": [
+ ":heavy_heart_exclamation_mark_ornament:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "emotion",
+ "punctuation",
+ "symbol"
+ ]
+ },
"heart_eyes": {
"unicode": "1F60D",
"unicode_alternates": [],
@@ -7008,7 +14174,22 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "crush", "face", "infatuation", "like", "love", "valentines", "smiling", "heart", "lovestruck", "love", "flirt", "smile", "heart-shaped"],
+ "keywords": [
+ "affection",
+ "crush",
+ "face",
+ "infatuation",
+ "like",
+ "love",
+ "valentines",
+ "smiling",
+ "heart",
+ "lovestruck",
+ "love",
+ "flirt",
+ "smile",
+ "heart-shaped"
+ ],
"moji": "😍"
},
"heart_eyes_cat": {
@@ -7019,7 +14200,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "animal", "cats", "like", "love", "valentines", "lovestruck", "love", "heart"],
+ "keywords": [
+ "affection",
+ "animal",
+ "cats",
+ "like",
+ "love",
+ "valentines",
+ "lovestruck",
+ "love",
+ "heart"
+ ],
"moji": "😻"
},
"heart_tip": {
@@ -7028,9 +14219,16 @@
"name": "heart with tip on the left",
"shortname": ":heart_tip:",
"category": "celebration",
- "aliases": [":heart_with_tip_on_the_left:"],
+ "aliases": [
+ ":heart_with_tip_on_the_left:"
+ ],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines"]
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines"
+ ]
},
"heartbeat": {
"unicode": "1F493",
@@ -7040,7 +14238,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines"
+ ],
"moji": "💓"
},
"heartpulse": {
@@ -7051,29 +14254,44 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines"
+ ],
"moji": "💗"
},
"hearts": {
"unicode": "2665",
- "unicode_alternates": ["2665-FE0F"],
+ "unicode_alternates": [
+ "2665-FE0F"
+ ],
"name": "black heart suit",
"shortname": ":hearts:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cards", "poker"],
+ "keywords": [
+ "cards",
+ "poker"
+ ],
"moji": "♥"
},
"heavy_check_mark": {
"unicode": "2714",
- "unicode_alternates": ["2714-FE0F"],
+ "unicode_alternates": [
+ "2714-FE0F"
+ ],
"name": "heavy check mark",
"shortname": ":heavy_check_mark:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nike", "ok"],
+ "keywords": [
+ "nike",
+ "ok"
+ ],
"moji": "✔"
},
"heavy_division_sign": {
@@ -7084,7 +14302,11 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["calculation", "divide", "math"],
+ "keywords": [
+ "calculation",
+ "divide",
+ "math"
+ ],
"moji": "➗"
},
"heavy_dollar_sign": {
@@ -7095,7 +14317,18 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["currency", "money", "payment", "dollar", "currency", "money", "cash", "sale", "purchase", "value"],
+ "keywords": [
+ "currency",
+ "money",
+ "payment",
+ "dollar",
+ "currency",
+ "money",
+ "cash",
+ "sale",
+ "purchase",
+ "value"
+ ],
"moji": "💲"
},
"heavy_minus_sign": {
@@ -7106,18 +14339,26 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["calculation", "math"],
+ "keywords": [
+ "calculation",
+ "math"
+ ],
"moji": "➖"
},
"heavy_multiplication_x": {
"unicode": "2716",
- "unicode_alternates": ["2716-FE0F"],
+ "unicode_alternates": [
+ "2716-FE0F"
+ ],
"name": "heavy multiplication x",
"shortname": ":heavy_multiplication_x:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["calculation", "math"],
+ "keywords": [
+ "calculation",
+ "math"
+ ],
"moji": "✖"
},
"heavy_plus_sign": {
@@ -7128,7 +14369,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["calculation", "math"],
+ "keywords": [
+ "calculation",
+ "math"
+ ],
"moji": "➕"
},
"helicopter": {
@@ -7139,9 +14383,33 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "helicopter", "helo", "gyro", "gyrocopter"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "helicopter",
+ "helo",
+ "gyro",
+ "gyrocopter"
+ ],
"moji": "🚁"
},
+ "helmet_with_cross": {
+ "unicode": "26D1",
+ "unicode_alternates": "",
+ "name": "helmet with white cross",
+ "shortname": ":helmet_with_cross:",
+ "category": "people",
+ "aliases": [
+ ":helmet_with_white_cross:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "aid",
+ "face",
+ "hat",
+ "person"
+ ]
+ },
"herb": {
"unicode": "1F33F",
"unicode_alternates": [],
@@ -7150,7 +14418,19 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["grass", "lawn", "medicine", "plant", "vegetable", "weed", "herb", "spice", "plant", "cook", "cooking"],
+ "keywords": [
+ "grass",
+ "lawn",
+ "medicine",
+ "plant",
+ "vegetable",
+ "weed",
+ "herb",
+ "spice",
+ "plant",
+ "cook",
+ "cooking"
+ ],
"moji": "🌿"
},
"hibiscus": {
@@ -7161,7 +14441,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flowers", "plant", "vegetable", "hibiscus", "flower", "warm"],
+ "keywords": [
+ "flowers",
+ "plant",
+ "vegetable",
+ "hibiscus",
+ "flower",
+ "warm"
+ ],
"moji": "🌺"
},
"high_brightness": {
@@ -7172,7 +14459,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["light", "summer", "sun"],
+ "keywords": [
+ "light",
+ "summer",
+ "sun"
+ ],
"moji": "🔆"
},
"high_heel": {
@@ -7183,9 +14474,23 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fashion", "female", "shoes"],
+ "keywords": [
+ "fashion",
+ "female",
+ "shoes"
+ ],
"moji": "👠"
},
+ "hockey": {
+ "unicode": "1F3D2",
+ "unicode_alternates": "",
+ "name": "ice hockey stick and puck",
+ "shortname": ":hockey:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"hole": {
"unicode": "1F573",
"unicode_alternates": [],
@@ -7194,7 +14499,10 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["pit", "well"]
+ "keywords": [
+ "pit",
+ "well"
+ ]
},
"homes": {
"unicode": "1F3D8",
@@ -7202,9 +14510,19 @@
"name": "house buildings",
"shortname": ":homes:",
"category": "travel_places",
- "aliases": [":house_buildings:"],
- "aliases_ascii": [],
- "keywords": ["home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman"]
+ "aliases": [
+ ":house_buildings:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "home",
+ "residence",
+ "dwelling",
+ "mansion",
+ "bungalow",
+ "ranch",
+ "craftsman"
+ ]
},
"honey_pot": {
"unicode": "1F36F",
@@ -7214,7 +14532,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bees", "sweet", "honey", "pot", "bees", "pooh", "bear"],
+ "keywords": [
+ "bees",
+ "sweet",
+ "honey",
+ "pot",
+ "bees",
+ "pooh",
+ "bear"
+ ],
"moji": "🍯"
},
"horse": {
@@ -7225,7 +14551,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "brown"],
+ "keywords": [
+ "animal",
+ "brown"
+ ],
"moji": "🐴"
},
"horse_racing": {
@@ -7236,9 +14565,103 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "betting", "competition", "horse", "race", "racing", "jockey", "triple crown"],
+ "keywords": [
+ "animal",
+ "betting",
+ "competition",
+ "horse",
+ "race",
+ "racing",
+ "jockey",
+ "triple crown"
+ ],
"moji": "🏇"
},
+ "horse_racing_tone1": {
+ "unicode": "1F3C7-1F3FB",
+ "unicode_alternates": "",
+ "name": "horse racing tone 1",
+ "shortname": ":horse_racing_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "animal",
+ "betting",
+ "competition",
+ "race",
+ "jockey",
+ "triple crown"
+ ]
+ },
+ "horse_racing_tone2": {
+ "unicode": "1F3C7-1F3FC",
+ "unicode_alternates": "",
+ "name": "horse racing tone 2",
+ "shortname": ":horse_racing_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "animal",
+ "betting",
+ "competition",
+ "race",
+ "jockey",
+ "triple crown"
+ ]
+ },
+ "horse_racing_tone3": {
+ "unicode": "1F3C7-1F3FD",
+ "unicode_alternates": "",
+ "name": "horse racing tone 3",
+ "shortname": ":horse_racing_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "animal",
+ "betting",
+ "competition",
+ "race",
+ "jockey",
+ "triple crown"
+ ]
+ },
+ "horse_racing_tone4": {
+ "unicode": "1F3C7-1F3FE",
+ "unicode_alternates": "",
+ "name": "horse racing tone 4",
+ "shortname": ":horse_racing_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "animal",
+ "betting",
+ "competition",
+ "race",
+ "jockey",
+ "triple crown"
+ ]
+ },
+ "horse_racing_tone5": {
+ "unicode": "1F3C7-1F3FF",
+ "unicode_alternates": "",
+ "name": "horse racing tone 5",
+ "shortname": ":horse_racing_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "animal",
+ "betting",
+ "competition",
+ "race",
+ "jockey",
+ "triple crown"
+ ]
+ },
"hospital": {
"unicode": "1F3E5",
"unicode_alternates": [],
@@ -7247,7 +14670,12 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "doctor", "health", "surgery"],
+ "keywords": [
+ "building",
+ "doctor",
+ "health",
+ "surgery"
+ ],
"moji": "🏥"
},
"hot_pepper": {
@@ -7258,7 +14686,27 @@
"category": "food_drink",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "nature", "spicy", "chili", "cayenne", "habanero", "jalapeno"]
+ "keywords": [
+ "food",
+ "nature",
+ "spicy",
+ "chili",
+ "cayenne",
+ "habanero",
+ "jalapeno"
+ ]
+ },
+ "hotdog": {
+ "unicode": "1F32D",
+ "unicode_alternates": "",
+ "name": "hot dog",
+ "shortname": ":hotdog:",
+ "category": "foods",
+ "aliases": [
+ ":hot_dog:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
},
"hotel": {
"unicode": "1F3E8",
@@ -7268,29 +14716,50 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["accomodation", "building", "checkin", "whotel", "hotel", "motel", "holiday inn", "hospital"],
+ "keywords": [
+ "accomodation",
+ "building",
+ "checkin",
+ "whotel",
+ "hotel",
+ "motel",
+ "holiday inn",
+ "hospital"
+ ],
"moji": "🏨"
},
"hotsprings": {
"unicode": "2668",
- "unicode_alternates": ["2668-FE0F"],
+ "unicode_alternates": [
+ "2668-FE0F"
+ ],
"name": "hot springs",
"shortname": ":hotsprings:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bath", "relax", "warm"],
+ "keywords": [
+ "bath",
+ "relax",
+ "warm"
+ ],
"moji": "♨"
},
"hourglass": {
"unicode": "231B",
- "unicode_alternates": ["231B-FE0F"],
+ "unicode_alternates": [
+ "231B-FE0F"
+ ],
"name": "hourglass",
"shortname": ":hourglass:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clock", "oldschool", "time"],
+ "keywords": [
+ "clock",
+ "oldschool",
+ "time"
+ ],
"moji": "⌛"
},
"hourglass_flowing_sand": {
@@ -7301,7 +14770,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["countdown", "oldschool", "time"],
+ "keywords": [
+ "countdown",
+ "oldschool",
+ "time"
+ ],
"moji": "⏳"
},
"house": {
@@ -7312,7 +14785,18 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "home", "house", "home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman"],
+ "keywords": [
+ "building",
+ "home",
+ "house",
+ "home",
+ "residence",
+ "dwelling",
+ "mansion",
+ "bungalow",
+ "ranch",
+ "craftsman"
+ ],
"moji": "🏠"
},
"house_abandoned": {
@@ -7321,9 +14805,24 @@
"name": "derelict house building",
"shortname": ":house_abandoned:",
"category": "travel_places",
- "aliases": [":derelict_house_building:"],
- "aliases_ascii": [],
- "keywords": ["home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman", "boarded", "abandoned", "vacant", "run down", "shoddy"]
+ "aliases": [
+ ":derelict_house_building:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "home",
+ "residence",
+ "dwelling",
+ "mansion",
+ "bungalow",
+ "ranch",
+ "craftsman",
+ "boarded",
+ "abandoned",
+ "vacant",
+ "run down",
+ "shoddy"
+ ]
},
"house_with_garden": {
"unicode": "1F3E1",
@@ -7333,9 +14832,25 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["home", "nature", "plant"],
+ "keywords": [
+ "home",
+ "nature",
+ "plant"
+ ],
"moji": "🏡"
},
+ "hugging": {
+ "unicode": "1F917",
+ "unicode_alternates": "",
+ "name": "hugging face",
+ "shortname": ":hugging:",
+ "category": "people",
+ "aliases": [
+ ":hugging_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"hushed": {
"unicode": "1F62F",
"unicode_alternates": [],
@@ -7344,7 +14859,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "woo", "quiet", "hush", "whisper", "silent"],
+ "keywords": [
+ "face",
+ "woo",
+ "quiet",
+ "hush",
+ "whisper",
+ "silent"
+ ],
"moji": "😯"
},
"ice_cream": {
@@ -7355,9 +14877,37 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["desert", "food", "hot", "icecream", "ice", "cream", "dairy", "dessert", "cold", "soft", "serve", "cone", "waffle"],
+ "keywords": [
+ "desert",
+ "food",
+ "hot",
+ "icecream",
+ "ice",
+ "cream",
+ "dairy",
+ "dessert",
+ "cold",
+ "soft",
+ "serve",
+ "cone",
+ "waffle"
+ ],
"moji": "🍨"
},
+ "ice_skate": {
+ "unicode": "26F8",
+ "unicode_alternates": "",
+ "name": "ice skate",
+ "shortname": ":ice_skate:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "place",
+ "sport",
+ "travel"
+ ]
+ },
"icecream": {
"unicode": "1F366",
"unicode_alternates": [],
@@ -7366,9 +14916,39 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["desert", "food", "hot", "icecream", "ice", "cream", "dairy", "dessert", "cold", "soft", "serve", "cone", "yogurt"],
+ "keywords": [
+ "desert",
+ "food",
+ "hot",
+ "icecream",
+ "ice",
+ "cream",
+ "dairy",
+ "dessert",
+ "cold",
+ "soft",
+ "serve",
+ "cone",
+ "yogurt"
+ ],
"moji": "🍦"
},
+ "id": {
+ "unicode": "1F194",
+ "unicode_alternates": "",
+ "name": "squared id",
+ "shortname": ":id:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "purple-square",
+ "identification",
+ "identity",
+ "symbol",
+ "word"
+ ]
+ },
"ideograph_advantage": {
"unicode": "1F250",
"unicode_alternates": [],
@@ -7377,7 +14957,12 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "get", "kanji", "obtain"],
+ "keywords": [
+ "chinese",
+ "get",
+ "kanji",
+ "obtain"
+ ],
"moji": "🉐"
},
"imp": {
@@ -7388,7 +14973,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["angry", "devil", "evil", "horns", "cute", "devil"],
+ "keywords": [
+ "angry",
+ "devil",
+ "evil",
+ "horns",
+ "cute",
+ "devil"
+ ],
"moji": "👿"
},
"inbox_tray": {
@@ -7399,7 +14991,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents", "email"],
+ "keywords": [
+ "documents",
+ "email"
+ ],
"moji": "📥"
},
"incoming_envelope": {
@@ -7410,7 +15005,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["email", "inbox"],
+ "keywords": [
+ "email",
+ "inbox"
+ ],
"moji": "📨"
},
"info": {
@@ -7419,9 +15017,13 @@
"name": "circled information source",
"shortname": ":info:",
"category": "objects_symbols",
- "aliases": [":circled_information_source:"],
+ "aliases": [
+ ":circled_information_source:"
+ ],
"aliases_ascii": [],
- "keywords": ["icon"]
+ "keywords": [
+ "icon"
+ ]
},
"information_desk_person": {
"unicode": "1F481",
@@ -7431,18 +15033,147 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girl", "human", "woman", "information", "help", "question", "answer", "sassy", "unimpressed", "attitude", "snarky"],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "woman",
+ "information",
+ "help",
+ "question",
+ "answer",
+ "sassy",
+ "unimpressed",
+ "attitude",
+ "snarky"
+ ],
"moji": "💁"
},
+ "information_desk_person_tone1": {
+ "unicode": "1F481-1F3FB",
+ "unicode_alternates": "",
+ "name": "information desk person tone 1",
+ "shortname": ":information_desk_person_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "woman",
+ "help",
+ "question",
+ "answer",
+ "sassy",
+ "unimpressed",
+ "attitude",
+ "snarky"
+ ]
+ },
+ "information_desk_person_tone2": {
+ "unicode": "1F481-1F3FC",
+ "unicode_alternates": "",
+ "name": "information desk person tone 2",
+ "shortname": ":information_desk_person_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "woman",
+ "help",
+ "question",
+ "answer",
+ "sassy",
+ "unimpressed",
+ "attitude",
+ "snarky"
+ ]
+ },
+ "information_desk_person_tone3": {
+ "unicode": "1F481-1F3FD",
+ "unicode_alternates": "",
+ "name": "information desk person tone 3",
+ "shortname": ":information_desk_person_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "woman",
+ "help",
+ "question",
+ "answer",
+ "sassy",
+ "unimpressed",
+ "attitude",
+ "snarky"
+ ]
+ },
+ "information_desk_person_tone4": {
+ "unicode": "1F481-1F3FE",
+ "unicode_alternates": "",
+ "name": "information desk person tone 4",
+ "shortname": ":information_desk_person_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "woman",
+ "help",
+ "question",
+ "answer",
+ "sassy",
+ "unimpressed",
+ "attitude",
+ "snarky"
+ ]
+ },
+ "information_desk_person_tone5": {
+ "unicode": "1F481-1F3FF",
+ "unicode_alternates": "",
+ "name": "information desk person tone 5",
+ "shortname": ":information_desk_person_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "woman",
+ "help",
+ "question",
+ "answer",
+ "sassy",
+ "unimpressed",
+ "attitude",
+ "snarky"
+ ]
+ },
"information_source": {
"unicode": "2139",
- "unicode_alternates": ["2139-FE0F"],
+ "unicode_alternates": [
+ "2139-FE0F"
+ ],
"name": "information source",
"shortname": ":information_source:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "blue-square", "letter"],
+ "keywords": [
+ "alphabet",
+ "blue-square",
+ "letter"
+ ],
"moji": "ℹ"
},
"innocent": {
@@ -7452,19 +15183,49 @@
"shortname": ":innocent:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["O:-)", "0:-3", "0:3", "0:-)", "0:)", "0;^)", "O:-)", "O:)", "O;-)", "O=)", "0;-)", "O:-3", "O:3"],
- "keywords": ["angel", "face", "halo", "halo", "angel", "innocent", "ring", "circle", "heaven"],
+ "aliases_ascii": [
+ "O:-)",
+ "0:-3",
+ "0:3",
+ "0:-)",
+ "0:)",
+ "0;^)",
+ "O:-)",
+ "O:)",
+ "O;-)",
+ "O=)",
+ "0;-)",
+ "O:-3",
+ "O:3"
+ ],
+ "keywords": [
+ "angel",
+ "face",
+ "halo",
+ "halo",
+ "angel",
+ "innocent",
+ "ring",
+ "circle",
+ "heaven"
+ ],
"moji": "😇"
},
"interrobang": {
"unicode": "2049",
- "unicode_alternates": ["2049-FE0F"],
+ "unicode_alternates": [
+ "2049-FE0F"
+ ],
"name": "exclamation question mark",
"shortname": ":interrobang:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["punctuation", "surprise", "wat"],
+ "keywords": [
+ "punctuation",
+ "surprise",
+ "wat"
+ ],
"moji": "⁉"
},
"iphone": {
@@ -7475,7 +15236,12 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["apple", "dial", "gadgets", "technology"],
+ "keywords": [
+ "apple",
+ "dial",
+ "gadgets",
+ "technology"
+ ],
"moji": "📱"
},
"island": {
@@ -7484,9 +15250,15 @@
"name": "desert island",
"shortname": ":island:",
"category": "travel_places",
- "aliases": [":desert_island:"],
+ "aliases": [
+ ":desert_island:"
+ ],
"aliases_ascii": [],
- "keywords": ["land", "solitude", "alone"]
+ "keywords": [
+ "land",
+ "solitude",
+ "alone"
+ ]
},
"izakaya_lantern": {
"unicode": "1F3EE",
@@ -7496,7 +15268,17 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["light", "izakaya", "lantern", "stay", "drink", "alcohol", "bar", "sake", "restaurant"],
+ "keywords": [
+ "light",
+ "izakaya",
+ "lantern",
+ "stay",
+ "drink",
+ "alcohol",
+ "bar",
+ "sake",
+ "restaurant"
+ ],
"moji": "🏮"
},
"jack_o_lantern": {
@@ -7507,7 +15289,24 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["halloween", "jack-o-lantern", "pumpkin", "halloween", "holiday", "carve", "autumn", "fall", "october", "saints", "costume", "spooky", "horror", "scary", "scared", "dead"],
+ "keywords": [
+ "halloween",
+ "jack-o-lantern",
+ "pumpkin",
+ "halloween",
+ "holiday",
+ "carve",
+ "autumn",
+ "fall",
+ "october",
+ "saints",
+ "costume",
+ "spooky",
+ "horror",
+ "scary",
+ "scared",
+ "dead"
+ ],
"moji": "🎃"
},
"japan": {
@@ -7518,7 +15317,9 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nation"],
+ "keywords": [
+ "nation"
+ ],
"moji": "🗾"
},
"japanese_castle": {
@@ -7529,7 +15330,17 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "photo", "castle", "japanese", "residence", "royalty", "fort", "fortified", "fortress"],
+ "keywords": [
+ "building",
+ "photo",
+ "castle",
+ "japanese",
+ "residence",
+ "royalty",
+ "fort",
+ "fortified",
+ "fortress"
+ ],
"moji": "🏯"
},
"japanese_goblin": {
@@ -7540,7 +15351,24 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["evil", "mask", "red", "japanese", "tengu", "supernatural", "avian", "demon", "goblin", "mask", "theater", "nose", "frown", "mustache", "anger", "frustration"],
+ "keywords": [
+ "evil",
+ "mask",
+ "red",
+ "japanese",
+ "tengu",
+ "supernatural",
+ "avian",
+ "demon",
+ "goblin",
+ "mask",
+ "theater",
+ "nose",
+ "frown",
+ "mustache",
+ "anger",
+ "frustration"
+ ],
"moji": "👺"
},
"japanese_ogre": {
@@ -7551,7 +15379,21 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["monster", "japanese", "oni", "demon", "troll", "ogre", "folklore", "monster", "devil", "mask", "theater", "horns", "teeth"],
+ "keywords": [
+ "monster",
+ "japanese",
+ "oni",
+ "demon",
+ "troll",
+ "ogre",
+ "folklore",
+ "monster",
+ "devil",
+ "mask",
+ "theater",
+ "horns",
+ "teeth"
+ ],
"moji": "👹"
},
"jeans": {
@@ -7562,7 +15404,19 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fashion", "shopping", "jeans", "pants", "blue", "denim", "levi&#039;s", "levi", "designer", "work", "skinny"],
+ "keywords": [
+ "fashion",
+ "shopping",
+ "jeans",
+ "pants",
+ "blue",
+ "denim",
+ "levi&#039;s",
+ "levi",
+ "designer",
+ "work",
+ "skinny"
+ ],
"moji": "👖"
},
"jet_up": {
@@ -7571,9 +15425,13 @@
"name": "up-pointing military airplane",
"shortname": ":jet_up:",
"category": "travel_places",
- "aliases": [":up_pointing_military_airplane:"],
+ "aliases": [
+ ":up_pointing_military_airplane:"
+ ],
"aliases_ascii": [],
- "keywords": ["jet"]
+ "keywords": [
+ "jet"
+ ]
},
"joy": {
"unicode": "1F602",
@@ -7582,8 +15440,22 @@
"shortname": ":joy:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":')", ":'-)"],
- "keywords": ["cry", "face", "haha", "happy", "tears", "tears", "cry", "joy", "happy", "weep"],
+ "aliases_ascii": [
+ ":')",
+ ":'-)"
+ ],
+ "keywords": [
+ "cry",
+ "face",
+ "haha",
+ "happy",
+ "tears",
+ "tears",
+ "cry",
+ "joy",
+ "happy",
+ "weep"
+ ],
"moji": "😂"
},
"joy_cat": {
@@ -7594,7 +15466,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cats", "haha", "happy", "tears", "happy", "tears", "cry", "joy"],
+ "keywords": [
+ "animal",
+ "cats",
+ "haha",
+ "happy",
+ "tears",
+ "happy",
+ "tears",
+ "cry",
+ "joy"
+ ],
"moji": "😹"
},
"joystick": {
@@ -7605,7 +15487,21 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["games", "atari", "controller"]
+ "keywords": [
+ "games",
+ "atari",
+ "controller"
+ ]
+ },
+ "kaaba": {
+ "unicode": "1F54B",
+ "unicode_alternates": "",
+ "name": "kaaba",
+ "shortname": ":kaaba:",
+ "category": "travel",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
},
"key": {
"unicode": "1F511",
@@ -7615,7 +15511,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["door", "lock", "password"],
+ "keywords": [
+ "door",
+ "lock",
+ "password"
+ ],
"moji": "🔑"
},
"key2": {
@@ -7624,9 +15524,16 @@
"name": "old key",
"shortname": ":key2:",
"category": "objects_symbols",
- "aliases": [":old_key:"],
+ "aliases": [
+ ":old_key:"
+ ],
"aliases_ascii": [],
- "keywords": ["door", "lock", "password", "skeleton"]
+ "keywords": [
+ "door",
+ "lock",
+ "password",
+ "skeleton"
+ ]
},
"keyboard": {
"unicode": "1F5AE",
@@ -7634,9 +15541,16 @@
"name": "wired keyboard",
"shortname": ":keyboard:",
"category": "objects_symbols",
- "aliases": [":wired_keyboard:"],
+ "aliases": [
+ ":wired_keyboard:"
+ ],
"aliases_ascii": [],
- "keywords": ["typing", "keys", "input", "device"]
+ "keywords": [
+ "typing",
+ "keys",
+ "input",
+ "device"
+ ]
},
"keyboard_mouse": {
"unicode": "1F5A6",
@@ -7644,9 +15558,15 @@
"name": "keyboard and mouse",
"shortname": ":keyboard_mouse:",
"category": "objects_symbols",
- "aliases": [":keyboard_and_mouse:"],
+ "aliases": [
+ ":keyboard_and_mouse:"
+ ],
"aliases_ascii": [],
- "keywords": ["computer", "input", "desktop"]
+ "keywords": [
+ "computer",
+ "input",
+ "desktop"
+ ]
},
"keyboard_with_jacks": {
"unicode": "1F398",
@@ -7654,9 +15574,15 @@
"name": "musical keyboard with jacks",
"shortname": ":keyboard_with_jacks:",
"category": "objects_symbols",
- "aliases": [":musical_keyboard_with_jacks:"],
+ "aliases": [
+ ":musical_keyboard_with_jacks:"
+ ],
"aliases_ascii": [],
- "keywords": ["music", "instrument", "midi"]
+ "keywords": [
+ "music",
+ "instrument",
+ "midi"
+ ]
},
"keycap_ten": {
"unicode": "1F51F",
@@ -7666,7 +15592,11 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["10", "blue-square", "numbers"],
+ "keywords": [
+ "10",
+ "blue-square",
+ "numbers"
+ ],
"moji": "🔟"
},
"kimono": {
@@ -7677,7 +15607,13 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["dress", "fashion", "female", "japanese", "women"],
+ "keywords": [
+ "dress",
+ "fashion",
+ "female",
+ "japanese",
+ "women"
+ ],
"moji": "👘"
},
"kiss": {
@@ -7688,28 +15624,57 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "face", "like", "lips", "love", "valentines"],
+ "keywords": [
+ "affection",
+ "face",
+ "like",
+ "lips",
+ "love",
+ "valentines"
+ ],
"moji": "💋"
},
"kiss_mm": {
"unicode": "1F468-2764-1F48B-1F468",
- "unicode_alternates": ["1F468-200D-2764-FE0F-200D-1F48B-200D-1F468"],
+ "unicode_alternates": [
+ "1F468-200D-2764-FE0F-200D-1F48B-200D-1F468"
+ ],
"name": "kiss (man,man)",
"shortname": ":kiss_mm:",
"category": "people",
- "aliases": [":couplekiss_mm:"],
- "aliases_ascii": [],
- "keywords": ["dating", "like", "love", "marriage", "valentines", "couple"]
+ "aliases": [
+ ":couplekiss_mm:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "dating",
+ "like",
+ "love",
+ "marriage",
+ "valentines",
+ "couple"
+ ]
},
"kiss_ww": {
"unicode": "1F469-2764-1F48B-1F469",
- "unicode_alternates": ["1F469-200D-2764-FE0F-200D-1F48B-200D-1F469"],
+ "unicode_alternates": [
+ "1F469-200D-2764-FE0F-200D-1F48B-200D-1F469"
+ ],
"name": "kiss (woman,woman)",
"shortname": ":kiss_ww:",
"category": "people",
- "aliases": [":couplekiss_ww:"],
- "aliases_ascii": [],
- "keywords": ["dating", "like", "love", "marriage", "valentines", "couple"]
+ "aliases": [
+ ":couplekiss_ww:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "dating",
+ "like",
+ "love",
+ "marriage",
+ "valentines",
+ "couple"
+ ]
},
"kissing": {
"unicode": "1F617",
@@ -7719,7 +15684,19 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["3", "face", "infatuation", "like", "love", "valentines", "kissing", "kiss", "pucker", "lips", "smooch"],
+ "keywords": [
+ "3",
+ "face",
+ "infatuation",
+ "like",
+ "love",
+ "valentines",
+ "kissing",
+ "kiss",
+ "pucker",
+ "lips",
+ "smooch"
+ ],
"moji": "😗"
},
"kissing_cat": {
@@ -7730,7 +15707,15 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cats", "passion", "kiss", "puckered", "heart", "love"],
+ "keywords": [
+ "animal",
+ "cats",
+ "passion",
+ "kiss",
+ "puckered",
+ "heart",
+ "love"
+ ],
"moji": "😽"
},
"kissing_closed_eyes": {
@@ -7741,7 +15726,21 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "face", "infatuation", "like", "love", "valentines", "kissing", "kiss", "passion", "puckered", "heart", "love", "smooch"],
+ "keywords": [
+ "affection",
+ "face",
+ "infatuation",
+ "like",
+ "love",
+ "valentines",
+ "kissing",
+ "kiss",
+ "passion",
+ "puckered",
+ "heart",
+ "love",
+ "smooch"
+ ],
"moji": "😚"
},
"kissing_heart": {
@@ -7751,8 +15750,25 @@
"shortname": ":kissing_heart:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":*", ":-*", "=*", ":^*"],
- "keywords": ["affection", "face", "infatuation", "kiss", "blowing kiss", "heart", "love", "lips", "like", "love", "valentines"],
+ "aliases_ascii": [
+ ":*",
+ ":-*",
+ "=*",
+ ":^*"
+ ],
+ "keywords": [
+ "affection",
+ "face",
+ "infatuation",
+ "kiss",
+ "blowing kiss",
+ "heart",
+ "love",
+ "lips",
+ "like",
+ "love",
+ "valentines"
+ ],
"moji": "😘"
},
"kissing_smiling_eyes": {
@@ -7763,7 +15779,18 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "face", "infatuation", "valentines", "kissing", "kiss", "smile", "pucker", "lips", "smooch"],
+ "keywords": [
+ "affection",
+ "face",
+ "infatuation",
+ "valentines",
+ "kissing",
+ "kiss",
+ "smile",
+ "pucker",
+ "lips",
+ "smooch"
+ ],
"moji": "😙"
},
"knife": {
@@ -7785,7 +15812,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐨"
},
"koko": {
@@ -7796,7 +15826,13 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "destination", "here", "japanese", "katakana"],
+ "keywords": [
+ "blue-square",
+ "destination",
+ "here",
+ "japanese",
+ "katakana"
+ ],
"moji": "🈁"
},
"label": {
@@ -7807,7 +15843,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["tag"]
+ "keywords": [
+ "tag"
+ ]
},
"large_blue_circle": {
"unicode": "1F535",
@@ -7828,7 +15866,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "🔷"
},
"large_orange_diamond": {
@@ -7839,7 +15879,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "🔶"
},
"last_quarter_moon": {
@@ -7850,7 +15892,16 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "last", "quarter", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "last",
+ "quarter",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌗"
},
"last_quarter_moon_with_face": {
@@ -7861,7 +15912,18 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "last", "quarter", "anthropomorphic", "face", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "last",
+ "quarter",
+ "anthropomorphic",
+ "face",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌜"
},
"laughing": {
@@ -7870,9 +15932,23 @@
"name": "smiling face with open mouth and tightly-closed ey",
"shortname": ":laughing:",
"category": "emoticons",
- "aliases": [":satisfied:"],
- "aliases_ascii": [">:)", ">;)", ">:-)", ">=)"],
- "keywords": ["happy", "joy", "lol", "smiling", "laughing", "laugh"],
+ "aliases": [
+ ":satisfied:"
+ ],
+ "aliases_ascii": [
+ ">:)",
+ ">;)",
+ ">:-)",
+ ">=)"
+ ],
+ "keywords": [
+ "happy",
+ "joy",
+ "lol",
+ "smiling",
+ "laughing",
+ "laugh"
+ ],
"moji": "😆"
},
"leaves": {
@@ -7883,7 +15959,19 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["grass", "lawn", "nature", "plant", "tree", "vegetable", "leaves", "leaf", "wind", "float", "fluttering"],
+ "keywords": [
+ "grass",
+ "lawn",
+ "nature",
+ "plant",
+ "tree",
+ "vegetable",
+ "leaves",
+ "leaf",
+ "wind",
+ "float",
+ "fluttering"
+ ],
"moji": "🍃"
},
"ledger": {
@@ -7894,7 +15982,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["notes", "paper"],
+ "keywords": [
+ "notes",
+ "paper"
+ ],
"moji": "📒"
},
"left_luggage": {
@@ -7905,7 +15996,14 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "travel", "bag", "baggage", "luggage", "travel"],
+ "keywords": [
+ "blue-square",
+ "travel",
+ "bag",
+ "baggage",
+ "luggage",
+ "travel"
+ ],
"moji": "🛅"
},
"left_receiver": {
@@ -7914,24 +16012,36 @@
"name": "left hand telephone receiver",
"shortname": ":left_receiver:",
"category": "objects_symbols",
- "aliases": [":left_hand_telephone_receiver:"],
+ "aliases": [
+ ":left_hand_telephone_receiver:"
+ ],
"aliases_ascii": [],
- "keywords": ["communication", "dial", "technology"]
+ "keywords": [
+ "communication",
+ "dial",
+ "technology"
+ ]
},
"left_right_arrow": {
"unicode": "2194",
- "unicode_alternates": ["2194-FE0F"],
+ "unicode_alternates": [
+ "2194-FE0F"
+ ],
"name": "left right arrow",
"shortname": ":left_right_arrow:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "↔"
},
"leftwards_arrow_with_hook": {
"unicode": "21A9",
- "unicode_alternates": ["21A9-FE0F"],
+ "unicode_alternates": [
+ "21A9-FE0F"
+ ],
"name": "leftwards arrow with hook",
"shortname": ":leftwards_arrow_with_hook:",
"category": "other",
@@ -7948,18 +16058,39 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fruit", "nature", "lemon", "yellow", "citrus"],
+ "keywords": [
+ "fruit",
+ "nature",
+ "lemon",
+ "yellow",
+ "citrus"
+ ],
"moji": "🍋"
},
"leo": {
"unicode": "264C",
- "unicode_alternates": ["264C-FE0F"],
+ "unicode_alternates": [
+ "264C-FE0F"
+ ],
"name": "leo",
"shortname": ":leo:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["leo", "lion", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "leo",
+ "lion",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "purple-square",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♌"
},
"leopard": {
@@ -7970,7 +16101,15 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "leopard", "cat", "spot", "spotted", "sexy"],
+ "keywords": [
+ "animal",
+ "nature",
+ "leopard",
+ "cat",
+ "spot",
+ "spotted",
+ "sexy"
+ ],
"moji": "🐆"
},
"level_slider": {
@@ -7981,7 +16120,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["controls"]
+ "keywords": [
+ "controls"
+ ]
},
"levitate": {
"unicode": "1F574",
@@ -7989,19 +16130,39 @@
"name": "man in business suit levitating",
"shortname": ":levitate:",
"category": "people",
- "aliases": [":man_in_business_suit_levitating:"],
+ "aliases": [
+ ":man_in_business_suit_levitating:"
+ ],
"aliases_ascii": [],
- "keywords": ["hover", "exclamation"]
+ "keywords": [
+ "hover",
+ "exclamation"
+ ]
},
"libra": {
"unicode": "264E",
- "unicode_alternates": ["264E-FE0F"],
+ "unicode_alternates": [
+ "264E-FE0F"
+ ],
"name": "libra",
"shortname": ":libra:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["libra", "scales", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "libra",
+ "scales",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "purple-square",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♎"
},
"lifter": {
@@ -8010,9 +16171,101 @@
"name": "weight lifter",
"shortname": ":lifter:",
"category": "activity",
- "aliases": [":weight_lifter:"],
+ "aliases": [
+ ":weight_lifter:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "bench",
+ "press",
+ "squats",
+ "deadlift"
+ ]
+ },
+ "lifter_tone1": {
+ "unicode": "1F3CB-1F3FB",
+ "unicode_alternates": "",
+ "name": "weight lifter tone 1",
+ "shortname": ":lifter_tone1:",
+ "category": "activity",
+ "aliases": [
+ ":weight_lifter_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "bench",
+ "press",
+ "squats",
+ "deadlift"
+ ]
+ },
+ "lifter_tone2": {
+ "unicode": "1F3CB-1F3FC",
+ "unicode_alternates": "",
+ "name": "weight lifter tone 2",
+ "shortname": ":lifter_tone2:",
+ "category": "activity",
+ "aliases": [
+ ":weight_lifter_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "bench",
+ "press",
+ "squats",
+ "deadlift"
+ ]
+ },
+ "lifter_tone3": {
+ "unicode": "1F3CB-1F3FD",
+ "unicode_alternates": "",
+ "name": "weight lifter tone 3",
+ "shortname": ":lifter_tone3:",
+ "category": "activity",
+ "aliases": [
+ ":weight_lifter_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "bench",
+ "press",
+ "squats",
+ "deadlift"
+ ]
+ },
+ "lifter_tone4": {
+ "unicode": "1F3CB-1F3FE",
+ "unicode_alternates": "",
+ "name": "weight lifter tone 4",
+ "shortname": ":lifter_tone4:",
+ "category": "activity",
+ "aliases": [
+ ":weight_lifter_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "bench",
+ "press",
+ "squats",
+ "deadlift"
+ ]
+ },
+ "lifter_tone5": {
+ "unicode": "1F3CB-1F3FF",
+ "unicode_alternates": "",
+ "name": "weight lifter tone 5",
+ "shortname": ":lifter_tone5:",
+ "category": "activity",
+ "aliases": [
+ ":weight_lifter_tone5:"
+ ],
"aliases_ascii": [],
- "keywords": ["bench", "press", "squats", "deadlift"]
+ "keywords": [
+ "bench",
+ "press",
+ "squats",
+ "deadlift"
+ ]
},
"light_check_mark": {
"unicode": "1F5F8",
@@ -8020,9 +16273,13 @@
"name": "light check mark",
"shortname": ":light_check_mark:",
"category": "objects_symbols",
- "aliases": [":light_mark:"],
+ "aliases": [
+ ":light_mark:"
+ ],
"aliases_ascii": [],
- "keywords": ["vote"]
+ "keywords": [
+ "vote"
+ ]
},
"light_rail": {
"unicode": "1F688",
@@ -8032,7 +16289,13 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "train", "rail", "light"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "train",
+ "rail",
+ "light"
+ ],
"moji": "🚈"
},
"link": {
@@ -8043,9 +16306,24 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["rings", "url"],
+ "keywords": [
+ "rings",
+ "url"
+ ],
"moji": "🔗"
},
+ "lion_face": {
+ "unicode": "1F981",
+ "unicode_alternates": "",
+ "name": "lion face",
+ "shortname": ":lion_face:",
+ "category": "nature",
+ "aliases": [
+ ":lion:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"lips": {
"unicode": "1F444",
"unicode_alternates": [],
@@ -8054,7 +16332,10 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["kiss", "mouth"],
+ "keywords": [
+ "kiss",
+ "mouth"
+ ],
"moji": "👄"
},
"lips2": {
@@ -8065,7 +16346,10 @@
"category": "people",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["kiss", "mouth"]
+ "keywords": [
+ "kiss",
+ "mouth"
+ ]
},
"lipstick": {
"unicode": "1F484",
@@ -8075,7 +16359,11 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fashion", "female", "girl"],
+ "keywords": [
+ "fashion",
+ "female",
+ "girl"
+ ],
"moji": "💄"
},
"lock": {
@@ -8086,7 +16374,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["password", "security"],
+ "keywords": [
+ "password",
+ "security"
+ ],
"moji": "🔒"
},
"lock_with_ink_pen": {
@@ -8097,7 +16388,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["secret", "security"],
+ "keywords": [
+ "secret",
+ "security"
+ ],
"moji": "🔏"
},
"lollipop": {
@@ -8108,7 +16402,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["candy", "food", "snack", "sweet", "lollipop", "stick", "lick", "sweet", "sugar", "candy"],
+ "keywords": [
+ "candy",
+ "food",
+ "snack",
+ "sweet",
+ "lollipop",
+ "stick",
+ "lick",
+ "sweet",
+ "sugar",
+ "candy"
+ ],
"moji": "🍭"
},
"loop": {
@@ -8119,7 +16424,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["curly"],
+ "keywords": [
+ "curly"
+ ],
"moji": "➿"
},
"loud_sound": {
@@ -8141,7 +16448,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sound", "volume"],
+ "keywords": [
+ "sound",
+ "volume"
+ ],
"moji": "📢"
},
"love_hotel": {
@@ -8152,7 +16462,22 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "dating", "like", "love", "hotel", "love", "sex", "romance", "leisure", "adultery", "prostitution", "hospital", "birth", "happy"],
+ "keywords": [
+ "affection",
+ "dating",
+ "like",
+ "love",
+ "hotel",
+ "love",
+ "sex",
+ "romance",
+ "leisure",
+ "adultery",
+ "prostitution",
+ "hospital",
+ "birth",
+ "happy"
+ ],
"moji": "🏩"
},
"love_letter": {
@@ -8163,7 +16488,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "email", "envelope", "like", "valentines", "love", "letter", "kiss", "heart"],
+ "keywords": [
+ "affection",
+ "email",
+ "envelope",
+ "like",
+ "valentines",
+ "love",
+ "letter",
+ "kiss",
+ "heart"
+ ],
"moji": "💌"
},
"low_brightness": {
@@ -8174,18 +16509,27 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["summer", "sun"],
+ "keywords": [
+ "summer",
+ "sun"
+ ],
"moji": "🔅"
},
"m": {
"unicode": "24C2",
- "unicode_alternates": ["24C2-FE0F"],
+ "unicode_alternates": [
+ "24C2-FE0F"
+ ],
"name": "circled latin capital letter m",
"shortname": ":m:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "blue-circle", "letter"],
+ "keywords": [
+ "alphabet",
+ "blue-circle",
+ "letter"
+ ],
"moji": "Ⓜ"
},
"mag": {
@@ -8196,7 +16540,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["search", "zoom", "detective", "investigator", "detail", "details"],
+ "keywords": [
+ "search",
+ "zoom",
+ "detective",
+ "investigator",
+ "detail",
+ "details"
+ ],
"moji": "🔍"
},
"mag_right": {
@@ -8207,18 +16558,31 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["search", "zoom", "detective", "investigator", "detail", "details"],
+ "keywords": [
+ "search",
+ "zoom",
+ "detective",
+ "investigator",
+ "detail",
+ "details"
+ ],
"moji": "🔎"
},
"mahjong": {
"unicode": "1F004",
- "unicode_alternates": ["1F004-FE0F"],
+ "unicode_alternates": [
+ "1F004-FE0F"
+ ],
"name": "mahjong tile red dragon",
"shortname": ":mahjong:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "game", "kanji"],
+ "keywords": [
+ "chinese",
+ "game",
+ "kanji"
+ ],
"moji": "🀄"
},
"mailbox": {
@@ -8229,7 +16593,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "email", "inbox"],
+ "keywords": [
+ "communication",
+ "email",
+ "inbox"
+ ],
"moji": "📫"
},
"mailbox_closed": {
@@ -8240,7 +16608,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "email", "inbox"],
+ "keywords": [
+ "communication",
+ "email",
+ "inbox"
+ ],
"moji": "📪"
},
"mailbox_with_mail": {
@@ -8251,7 +16623,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "email", "inbox"],
+ "keywords": [
+ "communication",
+ "email",
+ "inbox"
+ ],
"moji": "📬"
},
"mailbox_with_no_mail": {
@@ -8262,7 +16638,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["email", "inbox"],
+ "keywords": [
+ "email",
+ "inbox"
+ ],
"moji": "📭"
},
"man": {
@@ -8273,9 +16652,95 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["classy", "dad", "father", "guy", "mustashe"],
+ "keywords": [
+ "classy",
+ "dad",
+ "father",
+ "guy",
+ "mustashe"
+ ],
"moji": "👨"
},
+ "man_tone1": {
+ "unicode": "1F468-1F3FB",
+ "unicode_alternates": "",
+ "name": "man tone 1",
+ "shortname": ":man_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "classy",
+ "dad",
+ "father",
+ "guy",
+ "mustache"
+ ]
+ },
+ "man_tone2": {
+ "unicode": "1F468-1F3FC",
+ "unicode_alternates": "",
+ "name": "man tone 2",
+ "shortname": ":man_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "classy",
+ "dad",
+ "father",
+ "guy",
+ "mustache"
+ ]
+ },
+ "man_tone3": {
+ "unicode": "1F468-1F3FD",
+ "unicode_alternates": "",
+ "name": "man tone 3",
+ "shortname": ":man_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "classy",
+ "dad",
+ "father",
+ "guy",
+ "mustache"
+ ]
+ },
+ "man_tone4": {
+ "unicode": "1F468-1F3FE",
+ "unicode_alternates": "",
+ "name": "man tone 4",
+ "shortname": ":man_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "classy",
+ "dad",
+ "father",
+ "guy",
+ "mustache"
+ ]
+ },
+ "man_tone5": {
+ "unicode": "1F468-1F3FF",
+ "unicode_alternates": "",
+ "name": "man tone 5",
+ "shortname": ":man_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "classy",
+ "dad",
+ "father",
+ "guy",
+ "mustache"
+ ]
+ },
"man_with_gua_pi_mao": {
"unicode": "1F472",
"unicode_alternates": [],
@@ -8284,9 +16749,101 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["boy", "male", "skullcap", "chinese", "asian", "qing"],
+ "keywords": [
+ "boy",
+ "male",
+ "skullcap",
+ "chinese",
+ "asian",
+ "qing"
+ ],
"moji": "👲"
},
+ "man_with_gua_pi_mao_tone1": {
+ "unicode": "1F472-1F3FB",
+ "unicode_alternates": "",
+ "name": "man with gua pi mao tone 1",
+ "shortname": ":man_with_gua_pi_mao_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "skullcap",
+ "chinese",
+ "asian",
+ "qing"
+ ]
+ },
+ "man_with_gua_pi_mao_tone2": {
+ "unicode": "1F472-1F3FC",
+ "unicode_alternates": "",
+ "name": "man with gua pi mao tone 2",
+ "shortname": ":man_with_gua_pi_mao_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "skullcap",
+ "chinese",
+ "asian",
+ "qing"
+ ]
+ },
+ "man_with_gua_pi_mao_tone3": {
+ "unicode": "1F472-1F3FD",
+ "unicode_alternates": "",
+ "name": "man with gua pi mao tone 3",
+ "shortname": ":man_with_gua_pi_mao_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "skullcap",
+ "chinese",
+ "asian",
+ "qing"
+ ]
+ },
+ "man_with_gua_pi_mao_tone4": {
+ "unicode": "1F472-1F3FE",
+ "unicode_alternates": "",
+ "name": "man with gua pi mao tone 4",
+ "shortname": ":man_with_gua_pi_mao_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "skullcap",
+ "chinese",
+ "asian",
+ "qing"
+ ]
+ },
+ "man_with_gua_pi_mao_tone5": {
+ "unicode": "1F472-1F3FF",
+ "unicode_alternates": "",
+ "name": "man with gua pi mao tone 5",
+ "shortname": ":man_with_gua_pi_mao_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "boy",
+ "male",
+ "skullcap",
+ "chinese",
+ "asian",
+ "qing"
+ ]
+ },
"man_with_turban": {
"unicode": "1F473",
"unicode_alternates": [],
@@ -8295,9 +16852,120 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["male", "turban", "headdress", "headwear", "pagri", "india", "indian", "mummy", "wisdom", "peace"],
+ "keywords": [
+ "male",
+ "turban",
+ "headdress",
+ "headwear",
+ "pagri",
+ "india",
+ "indian",
+ "mummy",
+ "wisdom",
+ "peace"
+ ],
"moji": "👳"
},
+ "man_with_turban_tone1": {
+ "unicode": "1F473-1F3FB",
+ "unicode_alternates": "",
+ "name": "man with turban tone 1",
+ "shortname": ":man_with_turban_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "headdress",
+ "headwear",
+ "pagri",
+ "india",
+ "indian",
+ "mummy",
+ "wisdom",
+ "peace"
+ ]
+ },
+ "man_with_turban_tone2": {
+ "unicode": "1F473-1F3FC",
+ "unicode_alternates": "",
+ "name": "man with turban tone 2",
+ "shortname": ":man_with_turban_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "headdress",
+ "headwear",
+ "pagri",
+ "india",
+ "indian",
+ "mummy",
+ "wisdom",
+ "peace"
+ ]
+ },
+ "man_with_turban_tone3": {
+ "unicode": "1F473-1F3FD",
+ "unicode_alternates": "",
+ "name": "man with turban tone 3",
+ "shortname": ":man_with_turban_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "headdress",
+ "headwear",
+ "pagri",
+ "india",
+ "indian",
+ "mummy",
+ "wisdom",
+ "peace"
+ ]
+ },
+ "man_with_turban_tone4": {
+ "unicode": "1F473-1F3FE",
+ "unicode_alternates": "",
+ "name": "man with turban tone 4",
+ "shortname": ":man_with_turban_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "headdress",
+ "headwear",
+ "pagri",
+ "india",
+ "indian",
+ "mummy",
+ "wisdom",
+ "peace"
+ ]
+ },
+ "man_with_turban_tone5": {
+ "unicode": "1F473-1F3FF",
+ "unicode_alternates": "",
+ "name": "man with turban tone 5",
+ "shortname": ":man_with_turban_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "headdress",
+ "headwear",
+ "pagri",
+ "india",
+ "indian",
+ "mummy",
+ "wisdom",
+ "peace"
+ ]
+ },
"mans_shoe": {
"unicode": "1F45E",
"unicode_alternates": [],
@@ -8306,7 +16974,10 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fashion", "male"],
+ "keywords": [
+ "fashion",
+ "male"
+ ],
"moji": "👞"
},
"map": {
@@ -8315,9 +16986,15 @@
"name": "world map",
"shortname": ":map:",
"category": "travel_places",
- "aliases": [":world_map:"],
+ "aliases": [
+ ":world_map:"
+ ],
"aliases_ascii": [],
- "keywords": ["atlas", "earth", "cartography"]
+ "keywords": [
+ "atlas",
+ "earth",
+ "cartography"
+ ]
},
"maple_leaf": {
"unicode": "1F341",
@@ -8327,7 +17004,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["canada", "nature", "plant", "vegetable", "maple", "leaf", "syrup", "canada", "tree"],
+ "keywords": [
+ "canada",
+ "nature",
+ "plant",
+ "vegetable",
+ "maple",
+ "leaf",
+ "syrup",
+ "canada",
+ "tree"
+ ],
"moji": "🍁"
},
"mask": {
@@ -8338,7 +17025,16 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "ill", "sick", "sick", "virus", "flu", "medical", "mask"],
+ "keywords": [
+ "face",
+ "ill",
+ "sick",
+ "sick",
+ "virus",
+ "flu",
+ "medical",
+ "mask"
+ ],
"moji": "😷"
},
"massage": {
@@ -8349,9 +17045,83 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girl", "woman"],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ],
"moji": "💆"
},
+ "massage_tone1": {
+ "unicode": "1F486-1F3FB",
+ "unicode_alternates": "",
+ "name": "face massage tone 1",
+ "shortname": ":massage_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "massage_tone2": {
+ "unicode": "1F486-1F3FC",
+ "unicode_alternates": "",
+ "name": "face massage tone 2",
+ "shortname": ":massage_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "massage_tone3": {
+ "unicode": "1F486-1F3FD",
+ "unicode_alternates": "",
+ "name": "face massage tone 3",
+ "shortname": ":massage_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "massage_tone4": {
+ "unicode": "1F486-1F3FE",
+ "unicode_alternates": "",
+ "name": "face massage tone 4",
+ "shortname": ":massage_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "massage_tone5": {
+ "unicode": "1F486-1F3FF",
+ "unicode_alternates": "",
+ "name": "face massage tone 5",
+ "shortname": ":massage_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
"meat_on_bone": {
"unicode": "1F356",
"unicode_alternates": [],
@@ -8360,7 +17130,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "good", "meat", "bone", "animal", "cooked"],
+ "keywords": [
+ "food",
+ "good",
+ "meat",
+ "bone",
+ "animal",
+ "cooked"
+ ],
"moji": "🍖"
},
"medal": {
@@ -8369,9 +17146,22 @@
"name": "sports medal",
"shortname": ":medal:",
"category": "activity",
- "aliases": [":sports_medal:"],
- "aliases_ascii": [],
- "keywords": ["award", "ceremony", "contest", "ftw", "place", "win", "first", "show", "reward", "achievement"]
+ "aliases": [
+ ":sports_medal:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "award",
+ "ceremony",
+ "contest",
+ "ftw",
+ "place",
+ "win",
+ "first",
+ "show",
+ "reward",
+ "achievement"
+ ]
},
"mega": {
"unicode": "1F4E3",
@@ -8381,7 +17171,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sound", "speaker", "volume"],
+ "keywords": [
+ "sound",
+ "speaker",
+ "volume"
+ ],
"moji": "📣"
},
"melon": {
@@ -8392,9 +17186,26 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "nature", "melon", "cantaloupe", "honeydew"],
+ "keywords": [
+ "food",
+ "fruit",
+ "nature",
+ "melon",
+ "cantaloupe",
+ "honeydew"
+ ],
"moji": "🍈"
},
+ "menorah": {
+ "unicode": "1F54E",
+ "unicode_alternates": "",
+ "name": "menorah with nine branches",
+ "shortname": ":menorah:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"mens": {
"unicode": "1F6B9",
"unicode_alternates": [],
@@ -8403,9 +17214,122 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["restroom", "toilet", "wc", "men", "bathroom", "restroom", "sign", "boy", "male", "avatar"],
+ "keywords": [
+ "restroom",
+ "toilet",
+ "wc",
+ "men",
+ "bathroom",
+ "restroom",
+ "sign",
+ "boy",
+ "male",
+ "avatar"
+ ],
"moji": "🚹"
},
+ "metal": {
+ "unicode": "1F918",
+ "unicode_alternates": "",
+ "name": "sign of the horns",
+ "shortname": ":metal:",
+ "category": "people",
+ "aliases": [
+ ":sign_of_the_horns:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "band",
+ "concert",
+ "fingers",
+ "rocknroll"
+ ]
+ },
+ "metal_tone1": {
+ "unicode": "1F918-1F3FB",
+ "unicode_alternates": "",
+ "name": "sign of the horns tone 1",
+ "shortname": ":metal_tone1:",
+ "category": "people",
+ "aliases": [
+ ":sign_of_the_horns_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "band",
+ "concert",
+ "fingers",
+ "rocknroll"
+ ]
+ },
+ "metal_tone2": {
+ "unicode": "1F918-1F3FC",
+ "unicode_alternates": "",
+ "name": "sign of the horns tone 2",
+ "shortname": ":metal_tone2:",
+ "category": "people",
+ "aliases": [
+ ":sign_of_the_horns_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "band",
+ "concert",
+ "fingers",
+ "rocknroll"
+ ]
+ },
+ "metal_tone3": {
+ "unicode": "1F918-1F3FD",
+ "unicode_alternates": "",
+ "name": "sign of the horns tone 3",
+ "shortname": ":metal_tone3:",
+ "category": "people",
+ "aliases": [
+ ":sign_of_the_horns_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "band",
+ "concert",
+ "fingers",
+ "rocknroll"
+ ]
+ },
+ "metal_tone4": {
+ "unicode": "1F918-1F3FE",
+ "unicode_alternates": "",
+ "name": "sign of the horns tone 4",
+ "shortname": ":metal_tone4:",
+ "category": "people",
+ "aliases": [
+ ":sign_of_the_horns_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "band",
+ "concert",
+ "fingers",
+ "rocknroll"
+ ]
+ },
+ "metal_tone5": {
+ "unicode": "1F918-1F3FF",
+ "unicode_alternates": "",
+ "name": "sign of the horns tone 5",
+ "shortname": ":metal_tone5:",
+ "category": "people",
+ "aliases": [
+ ":sign_of_the_horns_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "band",
+ "concert",
+ "fingers",
+ "rocknroll"
+ ]
+ },
"metro": {
"unicode": "1F687",
"unicode_alternates": [],
@@ -8414,7 +17338,17 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "mrt", "transportation", "tube", "underground", "metro", "subway", "underground", "train"],
+ "keywords": [
+ "blue-square",
+ "mrt",
+ "transportation",
+ "tube",
+ "underground",
+ "metro",
+ "subway",
+ "underground",
+ "train"
+ ],
"moji": "🚇"
},
"microphone": {
@@ -8425,7 +17359,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["PA", "music", "sound", "microphone", "mic", "audio", "sound", "voice", "karaoke"],
+ "keywords": [
+ "PA",
+ "music",
+ "sound",
+ "microphone",
+ "mic",
+ "audio",
+ "sound",
+ "voice",
+ "karaoke"
+ ],
"moji": "🎤"
},
"microphone2": {
@@ -8434,9 +17378,15 @@
"name": "studio microphone",
"shortname": ":microphone2:",
"category": "objects_symbols",
- "aliases": [":studio_microphone:"],
+ "aliases": [
+ ":studio_microphone:"
+ ],
"aliases_ascii": [],
- "keywords": ["mic", "audio", "recording"]
+ "keywords": [
+ "mic",
+ "audio",
+ "recording"
+ ]
},
"microscope": {
"unicode": "1F52C",
@@ -8446,7 +17396,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["experiment", "laboratory", "zoomin"],
+ "keywords": [
+ "experiment",
+ "laboratory",
+ "zoomin"
+ ],
"moji": "🔬"
},
"middle_finger": {
@@ -8455,9 +17409,83 @@
"name": "reversed hand with middle finger extended",
"shortname": ":middle_finger:",
"category": "people",
- "aliases": [":reversed_hand_with_middle_finger_extended:"],
+ "aliases": [
+ ":reversed_hand_with_middle_finger_extended:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "fu"
+ ]
+ },
+ "middle_finger_tone1": {
+ "unicode": "1F595-1F3FB",
+ "unicode_alternates": "",
+ "name": "reversed hand with middle finger extended tone 1",
+ "shortname": ":middle_finger_tone1:",
+ "category": "people",
+ "aliases": [
+ ":reversed_hand_with_middle_finger_extended_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "fu"
+ ]
+ },
+ "middle_finger_tone2": {
+ "unicode": "1F595-1F3FC",
+ "unicode_alternates": "",
+ "name": "reversed hand with middle finger extended tone 2",
+ "shortname": ":middle_finger_tone2:",
+ "category": "people",
+ "aliases": [
+ ":reversed_hand_with_middle_finger_extended_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "fu"
+ ]
+ },
+ "middle_finger_tone3": {
+ "unicode": "1F595-1F3FD",
+ "unicode_alternates": "",
+ "name": "reversed hand with middle finger extended tone 3",
+ "shortname": ":middle_finger_tone3:",
+ "category": "people",
+ "aliases": [
+ ":reversed_hand_with_middle_finger_extended_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "fu"
+ ]
+ },
+ "middle_finger_tone4": {
+ "unicode": "1F595-1F3FE",
+ "unicode_alternates": "",
+ "name": "reversed hand with middle finger extended tone 4",
+ "shortname": ":middle_finger_tone4:",
+ "category": "people",
+ "aliases": [
+ ":reversed_hand_with_middle_finger_extended_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "fu"
+ ]
+ },
+ "middle_finger_tone5": {
+ "unicode": "1F595-1F3FF",
+ "unicode_alternates": "",
+ "name": "reversed hand with middle finger extended tone 5",
+ "shortname": ":middle_finger_tone5:",
+ "category": "people",
+ "aliases": [
+ ":reversed_hand_with_middle_finger_extended_tone5:"
+ ],
"aliases_ascii": [],
- "keywords": ["fu"]
+ "keywords": [
+ "fu"
+ ]
},
"military_medal": {
"unicode": "1F396",
@@ -8467,7 +17495,13 @@
"category": "celebration",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["honor", "acknowledgment", "purple heart", "heroism", "veteran"]
+ "keywords": [
+ "honor",
+ "acknowledgment",
+ "purple heart",
+ "heroism",
+ "veteran"
+ ]
},
"milky_way": {
"unicode": "1F30C",
@@ -8477,7 +17511,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["photo", "space", "milky", "galaxy", "star", "stars", "planets", "space", "sky"],
+ "keywords": [
+ "photo",
+ "space",
+ "milky",
+ "galaxy",
+ "star",
+ "stars",
+ "planets",
+ "space",
+ "sky"
+ ],
"moji": "🌌"
},
"minibus": {
@@ -8488,7 +17532,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["car", "transportation", "vehicle", "bus", "city", "transport", "transportation"],
+ "keywords": [
+ "car",
+ "transportation",
+ "vehicle",
+ "bus",
+ "city",
+ "transport",
+ "transportation"
+ ],
"moji": "🚐"
},
"minidisc": {
@@ -8499,7 +17551,13 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["data", "disc", "disk", "record", "technology"],
+ "keywords": [
+ "data",
+ "disc",
+ "disk",
+ "record",
+ "technology"
+ ],
"moji": "💽"
},
"mobile_phone_off": {
@@ -8510,9 +17568,23 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mute"],
+ "keywords": [
+ "mute"
+ ],
"moji": "📴"
},
+ "money_mouth": {
+ "unicode": "1F911",
+ "unicode_alternates": "",
+ "name": "money-mouth face",
+ "shortname": ":money_mouth:",
+ "category": "people",
+ "aliases": [
+ ":money_mouth_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"money_with_wings": {
"unicode": "1F4B8",
"unicode_alternates": [],
@@ -8521,7 +17593,22 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bills", "dollar", "payment", "money", "wings", "easy", "spend", "work", "lost", "blown", "burned", "gift", "cash", "dollar"],
+ "keywords": [
+ "bills",
+ "dollar",
+ "payment",
+ "money",
+ "wings",
+ "easy",
+ "spend",
+ "work",
+ "lost",
+ "blown",
+ "burned",
+ "gift",
+ "cash",
+ "dollar"
+ ],
"moji": "💸"
},
"moneybag": {
@@ -8532,7 +17619,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["coins", "dollar", "payment"],
+ "keywords": [
+ "coins",
+ "dollar",
+ "payment"
+ ],
"moji": "💰"
},
"monkey": {
@@ -8543,7 +17634,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "monkey", "primate", "banana", "silly"],
+ "keywords": [
+ "animal",
+ "nature",
+ "monkey",
+ "primate",
+ "banana",
+ "silly"
+ ],
"moji": "🐒"
},
"monkey_face": {
@@ -8554,7 +17652,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐵"
},
"monorail": {
@@ -8565,7 +17666,14 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "train", "mono", "rail", "transport"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "train",
+ "mono",
+ "rail",
+ "transport"
+ ],
"moji": "🚝"
},
"mood_bubble": {
@@ -8576,7 +17684,13 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["balloon", "conversation", "communication", "comic", "feeling"]
+ "keywords": [
+ "balloon",
+ "conversation",
+ "communication",
+ "comic",
+ "feeling"
+ ]
},
"mood_bubble_lightning": {
"unicode": "1F5F1",
@@ -8584,9 +17698,17 @@
"name": "lightning mood bubble",
"shortname": ":mood_bubble_lightning:",
"category": "objects_symbols",
- "aliases": [":lightning_mood_bubble:"],
- "aliases_ascii": [],
- "keywords": ["balloon", "conversation", "communication", "comic", "feeling"]
+ "aliases": [
+ ":lightning_mood_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "balloon",
+ "conversation",
+ "communication",
+ "comic",
+ "feeling"
+ ]
},
"mood_lightning": {
"unicode": "1F5F2",
@@ -8594,9 +17716,15 @@
"name": "lightning mood",
"shortname": ":mood_lightning:",
"category": "objects_symbols",
- "aliases": [":lightning_mood:"],
+ "aliases": [
+ ":lightning_mood:"
+ ],
"aliases_ascii": [],
- "keywords": ["zap", "electric", "current"]
+ "keywords": [
+ "zap",
+ "electric",
+ "current"
+ ]
},
"mortar_board": {
"unicode": "1F393",
@@ -8606,9 +17734,35 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cap", "college", "degree", "graduation", "hat", "school", "university", "graduation", "cap", "mortarboard", "academic", "education", "ceremony", "square", "tassel"],
+ "keywords": [
+ "cap",
+ "college",
+ "degree",
+ "graduation",
+ "hat",
+ "school",
+ "university",
+ "graduation",
+ "cap",
+ "mortarboard",
+ "academic",
+ "education",
+ "ceremony",
+ "square",
+ "tassel"
+ ],
"moji": "🎓"
},
+ "mosque": {
+ "unicode": "1F54C",
+ "unicode_alternates": "",
+ "name": "mosque",
+ "shortname": ":mosque:",
+ "category": "travel",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"motorboat": {
"unicode": "1F6E5",
"unicode_alternates": [],
@@ -8617,7 +17771,13 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "boat", "speedboat", "powerboat"]
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "boat",
+ "speedboat",
+ "powerboat"
+ ]
},
"motorcycle": {
"unicode": "1F3CD",
@@ -8625,9 +17785,14 @@
"name": "racing motorcycle",
"shortname": ":motorcycle:",
"category": "activity",
- "aliases": [":racing_motorcycle:"],
+ "aliases": [
+ ":racing_motorcycle:"
+ ],
"aliases_ascii": [],
- "keywords": ["bike", "speed"]
+ "keywords": [
+ "bike",
+ "speed"
+ ]
},
"motorway": {
"unicode": "1F6E3",
@@ -8637,7 +17802,13 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["road", "highway", "freeway", "traffic", "travel"]
+ "keywords": [
+ "road",
+ "highway",
+ "freeway",
+ "traffic",
+ "travel"
+ ]
},
"mount_fuji": {
"unicode": "1F5FB",
@@ -8647,9 +17818,26 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["japan", "mountain", "nature", "photo"],
+ "keywords": [
+ "japan",
+ "mountain",
+ "nature",
+ "photo"
+ ],
"moji": "🗻"
},
+ "mountain": {
+ "unicode": "26F0",
+ "unicode_alternates": "",
+ "name": "mountain",
+ "shortname": ":mountain:",
+ "category": "travel",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "place"
+ ]
+ },
"mountain_bicyclist": {
"unicode": "1F6B5",
"unicode_alternates": [],
@@ -8658,9 +17846,104 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["human", "sports", "transportation", "bicyclist", "mountain", "bike", "pedal", "bicycle", "transportation"],
+ "keywords": [
+ "human",
+ "sports",
+ "transportation",
+ "bicyclist",
+ "mountain",
+ "bike",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ],
"moji": "🚵"
},
+ "mountain_bicyclist_tone1": {
+ "unicode": "1F6B5-1F3FB",
+ "unicode_alternates": "",
+ "name": "mountain bicyclist tone 1",
+ "shortname": ":mountain_bicyclist_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "transportation",
+ "bike",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
+ "mountain_bicyclist_tone2": {
+ "unicode": "1F6B5-1F3FC",
+ "unicode_alternates": "",
+ "name": "mountain bicyclist tone 2",
+ "shortname": ":mountain_bicyclist_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "transportation",
+ "bike",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
+ "mountain_bicyclist_tone3": {
+ "unicode": "1F6B5-1F3FD",
+ "unicode_alternates": "",
+ "name": "mountain bicyclist tone 3",
+ "shortname": ":mountain_bicyclist_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "transportation",
+ "bike",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
+ "mountain_bicyclist_tone4": {
+ "unicode": "1F6B5-1F3FE",
+ "unicode_alternates": "",
+ "name": "mountain bicyclist tone 4",
+ "shortname": ":mountain_bicyclist_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "transportation",
+ "bike",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
+ "mountain_bicyclist_tone5": {
+ "unicode": "1F6B5-1F3FF",
+ "unicode_alternates": "",
+ "name": "mountain bicyclist tone 5",
+ "shortname": ":mountain_bicyclist_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "transportation",
+ "bike",
+ "pedal",
+ "bicycle",
+ "transportation"
+ ]
+ },
"mountain_cableway": {
"unicode": "1F6A0",
"unicode_alternates": [],
@@ -8669,7 +17952,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "mountain", "cable", "rail", "train", "railway"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "mountain",
+ "cable",
+ "rail",
+ "train",
+ "railway"
+ ],
"moji": "🚠"
},
"mountain_railway": {
@@ -8680,7 +17971,14 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "mountain", "railway", "rail", "train", "transport"],
+ "keywords": [
+ "transportation",
+ "mountain",
+ "railway",
+ "rail",
+ "train",
+ "transport"
+ ],
"moji": "🚞"
},
"mountain_snow": {
@@ -8689,9 +17987,16 @@
"name": "snow capped mountain",
"shortname": ":mountain_snow:",
"category": "travel_places",
- "aliases": [":snow_capped_mountain:"],
+ "aliases": [
+ ":snow_capped_mountain:"
+ ],
"aliases_ascii": [],
- "keywords": ["cold", "elevation", "hiking", "peak"]
+ "keywords": [
+ "cold",
+ "elevation",
+ "hiking",
+ "peak"
+ ]
},
"mouse": {
"unicode": "1F42D",
@@ -8701,7 +18006,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐭"
},
"mouse2": {
@@ -8712,7 +18020,13 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "mouse", "mice", "rodent"],
+ "keywords": [
+ "animal",
+ "nature",
+ "mouse",
+ "mice",
+ "rodent"
+ ],
"moji": "🐁"
},
"mouse_one": {
@@ -8721,9 +18035,32 @@
"name": "one button mouse",
"shortname": ":mouse_one:",
"category": "objects_symbols",
- "aliases": [":one_button_mouse:"],
+ "aliases": [
+ ":one_button_mouse:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "computer",
+ "input",
+ "device"
+ ]
+ },
+ "mouse_three_button": {
+ "unicode": "1F5B1",
+ "unicode_alternates": "",
+ "name": "three button mouse",
+ "shortname": ":mouse_three_button:",
+ "category": "objects",
+ "aliases": [
+ ":three_button_mouse:"
+ ],
"aliases_ascii": [],
- "keywords": ["computer", "input", "device"]
+ "keywords": [
+ "3",
+ "computer",
+ "object",
+ "office"
+ ]
},
"movie_camera": {
"unicode": "1F3A5",
@@ -8733,7 +18070,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["film", "record", "movie", "camera", "camcorder", "video", "motion", "picture"],
+ "keywords": [
+ "film",
+ "record",
+ "movie",
+ "camera",
+ "camcorder",
+ "video",
+ "motion",
+ "picture"
+ ],
"moji": "🎥"
},
"moyai": {
@@ -8744,7 +18090,10 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["island", "stone"],
+ "keywords": [
+ "island",
+ "stone"
+ ],
"moji": "🗿"
},
"muscle": {
@@ -8755,9 +18104,101 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arm", "flex", "hand", "strong", "muscle", "bicep"],
+ "keywords": [
+ "arm",
+ "flex",
+ "hand",
+ "strong",
+ "muscle",
+ "bicep"
+ ],
"moji": "💪"
},
+ "muscle_tone1": {
+ "unicode": "1F4AA-1F3FB",
+ "unicode_alternates": "",
+ "name": "flexed biceps tone 1",
+ "shortname": ":muscle_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arm",
+ "flex",
+ "hand",
+ "strong",
+ "muscle",
+ "bicep"
+ ]
+ },
+ "muscle_tone2": {
+ "unicode": "1F4AA-1F3FC",
+ "unicode_alternates": "",
+ "name": "flexed biceps tone 2",
+ "shortname": ":muscle_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arm",
+ "flex",
+ "hand",
+ "strong",
+ "muscle",
+ "bicep"
+ ]
+ },
+ "muscle_tone3": {
+ "unicode": "1F4AA-1F3FD",
+ "unicode_alternates": "",
+ "name": "flexed biceps tone 3",
+ "shortname": ":muscle_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arm",
+ "flex",
+ "hand",
+ "strong",
+ "muscle",
+ "bicep"
+ ]
+ },
+ "muscle_tone4": {
+ "unicode": "1F4AA-1F3FE",
+ "unicode_alternates": "",
+ "name": "flexed biceps tone 4",
+ "shortname": ":muscle_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arm",
+ "flex",
+ "hand",
+ "strong",
+ "muscle",
+ "bicep"
+ ]
+ },
+ "muscle_tone5": {
+ "unicode": "1F4AA-1F3FF",
+ "unicode_alternates": "",
+ "name": "flexed biceps tone 5",
+ "shortname": ":muscle_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arm",
+ "flex",
+ "hand",
+ "strong",
+ "muscle",
+ "bicep"
+ ]
+ },
"mushroom": {
"unicode": "1F344",
"unicode_alternates": [],
@@ -8766,7 +18207,14 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["plant", "vegetable", "mushroom", "fungi", "food", "fungus"],
+ "keywords": [
+ "plant",
+ "vegetable",
+ "mushroom",
+ "fungi",
+ "food",
+ "fungus"
+ ],
"moji": "🍄"
},
"musical_keyboard": {
@@ -8777,7 +18225,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["instrument", "piano", "music", "keyboard", "piano", "organ", "instrument", "electric"],
+ "keywords": [
+ "instrument",
+ "piano",
+ "music",
+ "keyboard",
+ "piano",
+ "organ",
+ "instrument",
+ "electric"
+ ],
"moji": "🎹"
},
"musical_note": {
@@ -8788,7 +18245,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["score", "musical", "music", "note", "music", "sound"],
+ "keywords": [
+ "score",
+ "musical",
+ "music",
+ "note",
+ "music",
+ "sound"
+ ],
"moji": "🎵"
},
"musical_score": {
@@ -8799,7 +18263,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["clef", "treble", "music", "musical", "score", "clef", "g-clef", "stave", "staff"],
+ "keywords": [
+ "clef",
+ "treble",
+ "music",
+ "musical",
+ "score",
+ "clef",
+ "g-clef",
+ "stave",
+ "staff"
+ ],
"moji": "🎼"
},
"mute": {
@@ -8810,7 +18284,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sound", "volume"],
+ "keywords": [
+ "sound",
+ "volume"
+ ],
"moji": "🔇"
},
"nail_care": {
@@ -8821,9 +18298,77 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["beauty", "manicure"],
+ "keywords": [
+ "beauty",
+ "manicure"
+ ],
"moji": "💅"
},
+ "nail_care_tone1": {
+ "unicode": "1F485-1F3FB",
+ "unicode_alternates": "",
+ "name": "nail polish tone 1",
+ "shortname": ":nail_care_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "beauty",
+ "manicure"
+ ]
+ },
+ "nail_care_tone2": {
+ "unicode": "1F485-1F3FC",
+ "unicode_alternates": "",
+ "name": "nail polish tone 2",
+ "shortname": ":nail_care_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "beauty",
+ "manicure"
+ ]
+ },
+ "nail_care_tone3": {
+ "unicode": "1F485-1F3FD",
+ "unicode_alternates": "",
+ "name": "nail polish tone 3",
+ "shortname": ":nail_care_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "beauty",
+ "manicure"
+ ]
+ },
+ "nail_care_tone4": {
+ "unicode": "1F485-1F3FE",
+ "unicode_alternates": "",
+ "name": "nail polish tone 4",
+ "shortname": ":nail_care_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "beauty",
+ "manicure"
+ ]
+ },
+ "nail_care_tone5": {
+ "unicode": "1F485-1F3FF",
+ "unicode_alternates": "",
+ "name": "nail polish tone 5",
+ "shortname": ":nail_care_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "beauty",
+ "manicure"
+ ]
+ },
"name_badge": {
"unicode": "1F4DB",
"unicode_alternates": [],
@@ -8832,7 +18377,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fire", "forbid"],
+ "keywords": [
+ "fire",
+ "forbid"
+ ],
"moji": "📛"
},
"necktie": {
@@ -8843,7 +18391,13 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cloth", "fashion", "formal", "shirt", "suitup"],
+ "keywords": [
+ "cloth",
+ "fashion",
+ "formal",
+ "shirt",
+ "suitup"
+ ],
"moji": "👔"
},
"negative_squared_cross_mark": {
@@ -8854,18 +18408,42 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["deny", "green-square", "no", "x"],
+ "keywords": [
+ "deny",
+ "green-square",
+ "no",
+ "x"
+ ],
"moji": "❎"
},
+ "nerd": {
+ "unicode": "1F913",
+ "unicode_alternates": "",
+ "name": "nerd face",
+ "shortname": ":nerd:",
+ "category": "people",
+ "aliases": [
+ ":nerd_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"network": {
"unicode": "1F5A7",
"unicode_alternates": [],
"name": "three networked computers",
"shortname": ":network:",
"category": "objects_symbols",
- "aliases": [":three_networked_computers:"],
+ "aliases": [
+ ":three_networked_computers:"
+ ],
"aliases_ascii": [],
- "keywords": ["lan", "wan", "network", "technology"]
+ "keywords": [
+ "lan",
+ "wan",
+ "network",
+ "technology"
+ ]
},
"neutral_face": {
"unicode": "1F610",
@@ -8875,7 +18453,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "indifference", "neutral", "objective", "impartial", "blank"],
+ "keywords": [
+ "face",
+ "indifference",
+ "neutral",
+ "objective",
+ "impartial",
+ "blank"
+ ],
"moji": "😐"
},
"new": {
@@ -8886,7 +18471,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "🆕"
},
"new_moon": {
@@ -8897,7 +18484,15 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "new", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "new",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌑"
},
"new_moon_with_face": {
@@ -8908,7 +18503,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "new", "anthropomorphic", "face", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "new",
+ "anthropomorphic",
+ "face",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌚"
},
"newspaper": {
@@ -8919,7 +18524,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["headline", "press"],
+ "keywords": [
+ "headline",
+ "press"
+ ],
"moji": "📰"
},
"newspaper2": {
@@ -8928,9 +18536,29 @@
"name": "rolled-up newspaper",
"shortname": ":newspaper2:",
"category": "objects_symbols",
- "aliases": [":rolled_up_newspaper:"],
- "aliases_ascii": [],
- "keywords": ["headline", "press"]
+ "aliases": [
+ ":rolled_up_newspaper:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "headline",
+ "press"
+ ]
+ },
+ "ng": {
+ "unicode": "1F196",
+ "unicode_alternates": "",
+ "name": "squared ng",
+ "shortname": ":ng:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "blue-square",
+ "no good",
+ "symbol",
+ "word"
+ ]
},
"night_with_stars": {
"unicode": "1F303",
@@ -8940,19 +18568,33 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["night", "star", "cloudless", "evening", "planets", "space", "sky"],
+ "keywords": [
+ "night",
+ "star",
+ "cloudless",
+ "evening",
+ "planets",
+ "space",
+ "sky"
+ ],
"moji": "🌃"
},
"nine": {
"moji": "9️⃣",
"unicode": "0039-20E3",
- "unicode_alternates": ["0039-FE0F-20E3"],
+ "unicode_alternates": [
+ "0039-FE0F-20E3"
+ ],
"name": "digit nine",
"shortname": ":nine:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["9", "blue-square", "numbers"]
+ "keywords": [
+ "9",
+ "blue-square",
+ "numbers"
+ ]
},
"no_bell": {
"unicode": "1F515",
@@ -8962,7 +18604,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mute", "sound", "volume"],
+ "keywords": [
+ "mute",
+ "sound",
+ "volume"
+ ],
"moji": "🔕"
},
"no_bicycles": {
@@ -8973,18 +18619,33 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cyclist", "prohibited", "bicycle", "bike pedal", "no"],
+ "keywords": [
+ "cyclist",
+ "prohibited",
+ "bicycle",
+ "bike pedal",
+ "no"
+ ],
"moji": "🚳"
},
"no_entry": {
"unicode": "26D4",
- "unicode_alternates": ["26D4-FE0F"],
+ "unicode_alternates": [
+ "26D4-FE0F"
+ ],
"name": "no entry",
"shortname": ":no_entry:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bad", "denied", "limit", "privacy", "security", "stop"],
+ "keywords": [
+ "bad",
+ "denied",
+ "limit",
+ "privacy",
+ "security",
+ "stop"
+ ],
"moji": "⛔"
},
"no_entry_sign": {
@@ -8995,7 +18656,16 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["denied", "disallow", "forbid", "limit", "stop", "no", "stop", "entry"],
+ "keywords": [
+ "denied",
+ "disallow",
+ "forbid",
+ "limit",
+ "stop",
+ "no",
+ "stop",
+ "entry"
+ ],
"moji": "🚫"
},
"no_good": {
@@ -9006,9 +18676,128 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girl", "woman", "no", "stop", "nope", "don&#039;t", "not"],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "no",
+ "stop",
+ "nope",
+ "don&#039;t",
+ "not"
+ ],
"moji": "🙅"
},
+ "no_good_tone1": {
+ "unicode": "1F645-1F3FB",
+ "unicode_alternates": "",
+ "name": "face with no good gesture tone 1",
+ "shortname": ":no_good_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "stop",
+ "nope",
+ "don't",
+ "not",
+ "forbidden",
+ "hand",
+ "person",
+ "prohibited"
+ ]
+ },
+ "no_good_tone2": {
+ "unicode": "1F645-1F3FC",
+ "unicode_alternates": "",
+ "name": "face with no good gesture tone 2",
+ "shortname": ":no_good_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "stop",
+ "nope",
+ "don't",
+ "not",
+ "forbidden",
+ "hand",
+ "person",
+ "prohibited"
+ ]
+ },
+ "no_good_tone3": {
+ "unicode": "1F645-1F3FD",
+ "unicode_alternates": "",
+ "name": "face with no good gesture tone 3",
+ "shortname": ":no_good_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "stop",
+ "nope",
+ "don't",
+ "not",
+ "forbidden",
+ "hand",
+ "person",
+ "prohibited"
+ ]
+ },
+ "no_good_tone4": {
+ "unicode": "1F645-1F3FE",
+ "unicode_alternates": "",
+ "name": "face with no good gesture tone 4",
+ "shortname": ":no_good_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "stop",
+ "nope",
+ "don't",
+ "not",
+ "forbidden",
+ "hand",
+ "person",
+ "prohibited"
+ ]
+ },
+ "no_good_tone5": {
+ "unicode": "1F645-1F3FF",
+ "unicode_alternates": "",
+ "name": "face with no good gesture tone 5",
+ "shortname": ":no_good_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "stop",
+ "nope",
+ "don't",
+ "not",
+ "forbidden",
+ "hand",
+ "person",
+ "prohibited"
+ ]
+ },
"no_mobile_phones": {
"unicode": "1F4F5",
"unicode_alternates": [],
@@ -9017,7 +18806,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["iphone", "mute"],
+ "keywords": [
+ "iphone",
+ "mute"
+ ],
"moji": "📵"
},
"no_mouth": {
@@ -9027,8 +18819,24 @@
"shortname": ":no_mouth:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":-X", ":X", ":-#", ":#", "=X", "=x", ":x", ":-x", "=#"],
- "keywords": ["face", "hellokitty", "mouth", "silent", "vapid"],
+ "aliases_ascii": [
+ ":-X",
+ ":X",
+ ":-#",
+ ":#",
+ "=X",
+ "=x",
+ ":x",
+ ":-x",
+ "=#"
+ ],
+ "keywords": [
+ "face",
+ "hellokitty",
+ "mouth",
+ "silent",
+ "vapid"
+ ],
"moji": "😶"
},
"no_pedestrians": {
@@ -9039,7 +18847,18 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["crossing", "rules", "walking", "no", "walk", "pedestrian", "stroll", "stride", "foot", "feet"],
+ "keywords": [
+ "crossing",
+ "rules",
+ "walking",
+ "no",
+ "walk",
+ "pedestrian",
+ "stroll",
+ "stride",
+ "foot",
+ "feet"
+ ],
"moji": "🚷"
},
"no_smoking": {
@@ -9050,7 +18869,18 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cigarette", "no", "smoking", "cigarette", "smoke", "cancer", "lungs", "inhale", "tar", "nicotine"],
+ "keywords": [
+ "cigarette",
+ "no",
+ "smoking",
+ "cigarette",
+ "smoke",
+ "cancer",
+ "lungs",
+ "inhale",
+ "tar",
+ "nicotine"
+ ],
"moji": "🚭"
},
"non-potable_water": {
@@ -9061,7 +18891,18 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["drink", "faucet", "tap", "non-potable", "water", "not drinkable", "dirty", "gross", "aqua", "h20"],
+ "keywords": [
+ "drink",
+ "faucet",
+ "tap",
+ "non-potable",
+ "water",
+ "not drinkable",
+ "dirty",
+ "gross",
+ "aqua",
+ "h20"
+ ],
"moji": "🚱"
},
"nose": {
@@ -9072,18 +18913,91 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["smell", "sniff"],
+ "keywords": [
+ "smell",
+ "sniff"
+ ],
"moji": "👃"
},
+ "nose_tone1": {
+ "unicode": "1F443-1F3FB",
+ "unicode_alternates": "",
+ "name": "nose tone 1",
+ "shortname": ":nose_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "smell",
+ "sniff"
+ ]
+ },
+ "nose_tone2": {
+ "unicode": "1F443-1F3FC",
+ "unicode_alternates": "",
+ "name": "nose tone 2",
+ "shortname": ":nose_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "smell",
+ "sniff"
+ ]
+ },
+ "nose_tone3": {
+ "unicode": "1F443-1F3FD",
+ "unicode_alternates": "",
+ "name": "nose tone 3",
+ "shortname": ":nose_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "smell",
+ "sniff"
+ ]
+ },
+ "nose_tone4": {
+ "unicode": "1F443-1F3FE",
+ "unicode_alternates": "",
+ "name": "nose tone 4",
+ "shortname": ":nose_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "smell",
+ "sniff"
+ ]
+ },
+ "nose_tone5": {
+ "unicode": "1F443-1F3FF",
+ "unicode_alternates": "",
+ "name": "nose tone 5",
+ "shortname": ":nose_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "smell",
+ "sniff"
+ ]
+ },
"note": {
"unicode": "1F5C9",
"unicode_alternates": [],
"name": "note page",
"shortname": ":note:",
"category": "objects_symbols",
- "aliases": [":note_page:"],
+ "aliases": [
+ ":note_page:"
+ ],
"aliases_ascii": [],
- "keywords": ["stationery", "post-it"]
+ "keywords": [
+ "stationery",
+ "post-it"
+ ]
},
"note_empty": {
"unicode": "1F5C6",
@@ -9091,9 +19005,14 @@
"name": "empty note page",
"shortname": ":note_empty:",
"category": "objects_symbols",
- "aliases": [":empty_note_page:"],
+ "aliases": [
+ ":empty_note_page:"
+ ],
"aliases_ascii": [],
- "keywords": ["stationery", "post-it"]
+ "keywords": [
+ "stationery",
+ "post-it"
+ ]
},
"notebook": {
"unicode": "1F4D3",
@@ -9103,7 +19022,12 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["notes", "paper", "record", "stationery"],
+ "keywords": [
+ "notes",
+ "paper",
+ "record",
+ "stationery"
+ ],
"moji": "📓"
},
"notebook_with_decorative_cover": {
@@ -9114,7 +19038,12 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["classroom", "notes", "paper", "record"],
+ "keywords": [
+ "classroom",
+ "notes",
+ "paper",
+ "record"
+ ],
"moji": "📔"
},
"notepad": {
@@ -9123,9 +19052,14 @@
"name": "note pad",
"shortname": ":notepad:",
"category": "objects_symbols",
- "aliases": [":note_pad:"],
+ "aliases": [
+ ":note_pad:"
+ ],
"aliases_ascii": [],
- "keywords": ["stationery", "post-it"]
+ "keywords": [
+ "stationery",
+ "post-it"
+ ]
},
"notepad_empty": {
"unicode": "1F5C7",
@@ -9133,9 +19067,14 @@
"name": "empty note pad",
"shortname": ":notepad_empty:",
"category": "objects_symbols",
- "aliases": [":empty_note_pad:"],
+ "aliases": [
+ ":empty_note_pad:"
+ ],
"aliases_ascii": [],
- "keywords": ["stationery", "post-it"]
+ "keywords": [
+ "stationery",
+ "post-it"
+ ]
},
"notepad_spiral": {
"unicode": "1F5D2",
@@ -9143,9 +19082,13 @@
"name": "spiral note pad",
"shortname": ":notepad_spiral:",
"category": "objects_symbols",
- "aliases": [":spiral_note_pad:"],
+ "aliases": [
+ ":spiral_note_pad:"
+ ],
"aliases_ascii": [],
- "keywords": ["stationery"]
+ "keywords": [
+ "stationery"
+ ]
},
"notes": {
"unicode": "1F3B6",
@@ -9155,7 +19098,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["music", "score", "musical", "music", "notes", "music", "sound", "melody"],
+ "keywords": [
+ "music",
+ "score",
+ "musical",
+ "music",
+ "notes",
+ "music",
+ "sound",
+ "melody"
+ ],
"moji": "🎶"
},
"nut_and_bolt": {
@@ -9166,18 +19118,26 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["handy", "tools"],
+ "keywords": [
+ "handy",
+ "tools"
+ ],
"moji": "🔩"
},
"o": {
"unicode": "2B55",
- "unicode_alternates": ["2B55-FE0F"],
+ "unicode_alternates": [
+ "2B55-FE0F"
+ ],
"name": "heavy large circle",
"shortname": ":o:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["circle", "round"],
+ "keywords": [
+ "circle",
+ "round"
+ ],
"moji": "⭕"
},
"o2": {
@@ -9188,7 +19148,11 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "letter", "red-square"],
+ "keywords": [
+ "alphabet",
+ "letter",
+ "red-square"
+ ],
"moji": "🅾"
},
"ocean": {
@@ -9199,7 +19163,16 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sea", "water", "wave", "ocean", "wave", "surf", "beach", "tide"],
+ "keywords": [
+ "sea",
+ "water",
+ "wave",
+ "ocean",
+ "wave",
+ "surf",
+ "beach",
+ "tide"
+ ],
"moji": "🌊"
},
"octopus": {
@@ -9210,7 +19183,12 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "creature", "ocean", "sea"],
+ "keywords": [
+ "animal",
+ "creature",
+ "ocean",
+ "sea"
+ ],
"moji": "🐙"
},
"oden": {
@@ -9221,7 +19199,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "japanese", "oden", "seafood", "casserole", "stew"],
+ "keywords": [
+ "food",
+ "japanese",
+ "oden",
+ "seafood",
+ "casserole",
+ "stew"
+ ],
"moji": "🍢"
},
"office": {
@@ -9232,7 +19217,11 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "bureau", "work"],
+ "keywords": [
+ "building",
+ "bureau",
+ "work"
+ ],
"moji": "🏢"
},
"oil": {
@@ -9241,9 +19230,13 @@
"name": "oil drum",
"shortname": ":oil:",
"category": "objects_symbols",
- "aliases": [":oil_drum:"],
+ "aliases": [
+ ":oil_drum:"
+ ],
"aliases_ascii": [],
- "keywords": ["petroleum"]
+ "keywords": [
+ "petroleum"
+ ]
},
"ok": {
"unicode": "1F197",
@@ -9253,7 +19246,12 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["agree", "blue-square", "good", "yes"],
+ "keywords": [
+ "agree",
+ "blue-square",
+ "good",
+ "yes"
+ ],
"moji": "🆗"
},
"ok_hand": {
@@ -9264,9 +19262,126 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fingers", "limbs", "perfect", "okay", "ok", "smoke", "smoking", "marijuana", "joint", "pot", "420"],
+ "keywords": [
+ "fingers",
+ "limbs",
+ "perfect",
+ "okay",
+ "ok",
+ "smoke",
+ "smoking",
+ "marijuana",
+ "joint",
+ "pot",
+ "420"
+ ],
"moji": "👌"
},
+ "ok_hand_tone1": {
+ "unicode": "1F44C-1F3FB",
+ "unicode_alternates": "",
+ "name": "ok hand sign tone 1",
+ "shortname": ":ok_hand_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "limbs",
+ "perfect",
+ "okay",
+ "smoke",
+ "smoking",
+ "marijuana",
+ "joint",
+ "pot",
+ "420"
+ ]
+ },
+ "ok_hand_tone2": {
+ "unicode": "1F44C-1F3FC",
+ "unicode_alternates": "",
+ "name": "ok hand sign tone 2",
+ "shortname": ":ok_hand_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "limbs",
+ "perfect",
+ "okay",
+ "smoke",
+ "smoking",
+ "marijuana",
+ "joint",
+ "pot",
+ "420"
+ ]
+ },
+ "ok_hand_tone3": {
+ "unicode": "1F44C-1F3FD",
+ "unicode_alternates": "",
+ "name": "ok hand sign tone 3",
+ "shortname": ":ok_hand_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "limbs",
+ "perfect",
+ "okay",
+ "smoke",
+ "smoking",
+ "marijuana",
+ "joint",
+ "pot",
+ "420"
+ ]
+ },
+ "ok_hand_tone4": {
+ "unicode": "1F44C-1F3FE",
+ "unicode_alternates": "",
+ "name": "ok hand sign tone 4",
+ "shortname": ":ok_hand_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "limbs",
+ "perfect",
+ "okay",
+ "smoke",
+ "smoking",
+ "marijuana",
+ "joint",
+ "pot",
+ "420"
+ ]
+ },
+ "ok_hand_tone5": {
+ "unicode": "1F44C-1F3FF",
+ "unicode_alternates": "",
+ "name": "ok hand sign tone 5",
+ "shortname": ":ok_hand_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "limbs",
+ "perfect",
+ "okay",
+ "smoke",
+ "smoking",
+ "marijuana",
+ "joint",
+ "pot",
+ "420"
+ ]
+ },
"ok_woman": {
"unicode": "1F646",
"unicode_alternates": [],
@@ -9274,10 +19389,120 @@
"shortname": ":ok_woman:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["*\\0/*", "\\0/", "*\\O/*", "\\O/"],
- "keywords": ["female", "girl", "human", "pink", "women", "yes", "ok", "okay", "accept"],
+ "aliases_ascii": [
+ "*\\0/*",
+ "\\0/",
+ "*\\O/*",
+ "\\O/"
+ ],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "pink",
+ "women",
+ "yes",
+ "ok",
+ "okay",
+ "accept"
+ ],
"moji": "🙆"
},
+ "ok_woman_tone1": {
+ "unicode": "1F646-1F3FB",
+ "unicode_alternates": "",
+ "name": "face with ok gesture tone1",
+ "shortname": ":ok_woman_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "pink",
+ "women",
+ "yes",
+ "okay",
+ "accept"
+ ]
+ },
+ "ok_woman_tone2": {
+ "unicode": "1F646-1F3FC",
+ "unicode_alternates": "",
+ "name": "face with ok gesture tone2",
+ "shortname": ":ok_woman_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "pink",
+ "women",
+ "yes",
+ "okay",
+ "accept"
+ ]
+ },
+ "ok_woman_tone3": {
+ "unicode": "1F646-1F3FD",
+ "unicode_alternates": "",
+ "name": "face with ok gesture tone3",
+ "shortname": ":ok_woman_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "pink",
+ "women",
+ "yes",
+ "okay",
+ "accept"
+ ]
+ },
+ "ok_woman_tone4": {
+ "unicode": "1F646-1F3FE",
+ "unicode_alternates": "",
+ "name": "face with ok gesture tone4",
+ "shortname": ":ok_woman_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "pink",
+ "women",
+ "yes",
+ "okay",
+ "accept"
+ ]
+ },
+ "ok_woman_tone5": {
+ "unicode": "1F646-1F3FF",
+ "unicode_alternates": "",
+ "name": "face with ok gesture tone5",
+ "shortname": ":ok_woman_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "human",
+ "pink",
+ "women",
+ "yes",
+ "okay",
+ "accept"
+ ]
+ },
"older_man": {
"unicode": "1F474",
"unicode_alternates": [],
@@ -9286,20 +19511,197 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["human", "male", "men"],
+ "keywords": [
+ "human",
+ "male",
+ "men"
+ ],
"moji": "👴"
},
+ "older_man_tone1": {
+ "unicode": "1F474-1F3FB",
+ "unicode_alternates": "",
+ "name": "older man tone 1",
+ "shortname": ":older_man_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "men",
+ "grandpa",
+ "grandfather"
+ ]
+ },
+ "older_man_tone2": {
+ "unicode": "1F474-1F3FC",
+ "unicode_alternates": "",
+ "name": "older man tone 2",
+ "shortname": ":older_man_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "men",
+ "grandpa",
+ "grandfather"
+ ]
+ },
+ "older_man_tone3": {
+ "unicode": "1F474-1F3FD",
+ "unicode_alternates": "",
+ "name": "older man tone 3",
+ "shortname": ":older_man_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "men",
+ "grandpa",
+ "grandfather"
+ ]
+ },
+ "older_man_tone4": {
+ "unicode": "1F474-1F3FE",
+ "unicode_alternates": "",
+ "name": "older man tone 4",
+ "shortname": ":older_man_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "men",
+ "grandpa",
+ "grandfather"
+ ]
+ },
+ "older_man_tone5": {
+ "unicode": "1F474-1F3FF",
+ "unicode_alternates": "",
+ "name": "older man tone 5",
+ "shortname": ":older_man_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "men",
+ "grandpa",
+ "grandfather"
+ ]
+ },
"older_woman": {
"unicode": "1F475",
"unicode_alternates": [],
"name": "older woman",
"shortname": ":older_woman:",
"category": "emoticons",
- "aliases": [":grandma:"],
- "aliases_ascii": [],
- "keywords": ["female", "girl", "women", "grandma", "grandmother"],
+ "aliases": [
+ ":grandma:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "women",
+ "grandma",
+ "grandmother"
+ ],
"moji": "👵"
},
+ "older_woman_tone1": {
+ "unicode": "1F475-1F3FB",
+ "unicode_alternates": "",
+ "name": "older woman tone 1",
+ "shortname": ":older_woman_tone1:",
+ "category": "people",
+ "aliases": [
+ ":grandma_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "women",
+ "lady",
+ "grandma",
+ "grandmother"
+ ]
+ },
+ "older_woman_tone2": {
+ "unicode": "1F475-1F3FC",
+ "unicode_alternates": "",
+ "name": "older woman tone 2",
+ "shortname": ":older_woman_tone2:",
+ "category": "people",
+ "aliases": [
+ ":grandma_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "women",
+ "lady",
+ "grandma",
+ "grandmother"
+ ]
+ },
+ "older_woman_tone3": {
+ "unicode": "1F475-1F3FD",
+ "unicode_alternates": "",
+ "name": "older woman tone 3",
+ "shortname": ":older_woman_tone3:",
+ "category": "people",
+ "aliases": [
+ ":grandma_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "women",
+ "lady",
+ "grandma",
+ "grandmother"
+ ]
+ },
+ "older_woman_tone4": {
+ "unicode": "1F475-1F3FE",
+ "unicode_alternates": "",
+ "name": "older woman tone 4",
+ "shortname": ":older_woman_tone4:",
+ "category": "people",
+ "aliases": [
+ ":grandma_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "women",
+ "lady",
+ "grandma",
+ "grandmother"
+ ]
+ },
+ "older_woman_tone5": {
+ "unicode": "1F475-1F3FF",
+ "unicode_alternates": "",
+ "name": "older woman tone 5",
+ "shortname": ":older_woman_tone5:",
+ "category": "people",
+ "aliases": [
+ ":grandma_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "women",
+ "lady",
+ "grandma",
+ "grandmother"
+ ]
+ },
"om_symbol": {
"unicode": "1F549",
"unicode_alternates": [],
@@ -9308,7 +19710,16 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["hinduism", "sound", "spiritual", "icon", "dharmic", "buddhism", "jainism", "meditate"]
+ "keywords": [
+ "hinduism",
+ "sound",
+ "spiritual",
+ "icon",
+ "dharmic",
+ "buddhism",
+ "jainism",
+ "meditate"
+ ]
},
"on": {
"unicode": "1F51B",
@@ -9318,7 +19729,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "words"],
+ "keywords": [
+ "arrow",
+ "words"
+ ],
"moji": "🔛"
},
"oncoming_automobile": {
@@ -9329,7 +19743,14 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["car", "transportation", "vehicle", "sedan", "car", "automobile"],
+ "keywords": [
+ "car",
+ "transportation",
+ "vehicle",
+ "sedan",
+ "car",
+ "automobile"
+ ],
"moji": "🚘"
},
"oncoming_bus": {
@@ -9340,7 +19761,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "bus", "school", "city", "transportation", "public"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "bus",
+ "school",
+ "city",
+ "transportation",
+ "public"
+ ],
"moji": "🚍"
},
"oncoming_police_car": {
@@ -9351,7 +19780,19 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["enforcement", "law", "vehicle", "police", "car", "emergency", "ticket", "citation", "crime", "help", "officer"],
+ "keywords": [
+ "enforcement",
+ "law",
+ "vehicle",
+ "police",
+ "car",
+ "emergency",
+ "ticket",
+ "citation",
+ "crime",
+ "help",
+ "officer"
+ ],
"moji": "🚔"
},
"oncoming_taxi": {
@@ -9362,19 +19803,35 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cars", "uber", "vehicle", "taxi", "car", "automobile", "city", "transport", "service"],
+ "keywords": [
+ "cars",
+ "uber",
+ "vehicle",
+ "taxi",
+ "car",
+ "automobile",
+ "city",
+ "transport",
+ "service"
+ ],
"moji": "🚖"
},
"one": {
"moji": "1️⃣",
"unicode": "0031-20E3",
- "unicode_alternates": ["0031-FE0F-20E3"],
+ "unicode_alternates": [
+ "0031-FE0F-20E3"
+ ],
"name": "digit one",
"shortname": ":one:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["1", "blue-square", "numbers"]
+ "keywords": [
+ "1",
+ "blue-square",
+ "numbers"
+ ]
},
"open_file_folder": {
"unicode": "1F4C2",
@@ -9384,7 +19841,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents", "load"],
+ "keywords": [
+ "documents",
+ "load"
+ ],
"moji": "📂"
},
"open_hands": {
@@ -9395,9 +19855,77 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["butterfly", "fingers"],
+ "keywords": [
+ "butterfly",
+ "fingers"
+ ],
"moji": "👐"
},
+ "open_hands_tone1": {
+ "unicode": "1F450-1F3FB",
+ "unicode_alternates": "",
+ "name": "open hands sign tone 1",
+ "shortname": ":open_hands_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "butterfly",
+ "fingers"
+ ]
+ },
+ "open_hands_tone2": {
+ "unicode": "1F450-1F3FC",
+ "unicode_alternates": "",
+ "name": "open hands sign tone 2",
+ "shortname": ":open_hands_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "butterfly",
+ "fingers"
+ ]
+ },
+ "open_hands_tone3": {
+ "unicode": "1F450-1F3FD",
+ "unicode_alternates": "",
+ "name": "open hands sign tone 3",
+ "shortname": ":open_hands_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "butterfly",
+ "fingers"
+ ]
+ },
+ "open_hands_tone4": {
+ "unicode": "1F450-1F3FE",
+ "unicode_alternates": "",
+ "name": "open hands sign tone 4",
+ "shortname": ":open_hands_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "butterfly",
+ "fingers"
+ ]
+ },
+ "open_hands_tone5": {
+ "unicode": "1F450-1F3FF",
+ "unicode_alternates": "",
+ "name": "open hands sign tone 5",
+ "shortname": ":open_hands_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "butterfly",
+ "fingers"
+ ]
+ },
"open_mouth": {
"unicode": "1F62E",
"unicode_alternates": [],
@@ -9405,8 +19933,24 @@
"shortname": ":open_mouth:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":-O", ":O", ":-o", ":o", "O_O", ">:O"],
- "keywords": ["face", "impressed", "mouth", "open", "jaw", "gapping", "surprise", "wow"],
+ "aliases_ascii": [
+ ":-O",
+ ":O",
+ ":-o",
+ ":o",
+ "O_O",
+ ">:O"
+ ],
+ "keywords": [
+ "face",
+ "impressed",
+ "mouth",
+ "open",
+ "jaw",
+ "gapping",
+ "surprise",
+ "wow"
+ ],
"moji": "😮"
},
"ophiuchus": {
@@ -9417,7 +19961,19 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["ophiuchus", "serpent", "snake", "astrology", "greek", "constellation", "stars", "zodiac", "purple-square", "sign", "horoscope"],
+ "keywords": [
+ "ophiuchus",
+ "serpent",
+ "snake",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "purple-square",
+ "sign",
+ "horoscope"
+ ],
"moji": "⛎"
},
"optical_disk": {
@@ -9426,9 +19982,17 @@
"name": "optical disc icon",
"shortname": ":optical_disk:",
"category": "objects_symbols",
- "aliases": [":optical_disc_icon:"],
- "aliases_ascii": [],
- "keywords": ["cd", "dvd", "disc", "disk", "technology"]
+ "aliases": [
+ ":optical_disc_icon:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cd",
+ "dvd",
+ "disc",
+ "disk",
+ "technology"
+ ]
},
"orange_book": {
"unicode": "1F4D9",
@@ -9438,9 +20002,27 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["knowledge", "library", "read"],
+ "keywords": [
+ "knowledge",
+ "library",
+ "read"
+ ],
"moji": "📙"
},
+ "orthodox_cross": {
+ "unicode": "2626",
+ "unicode_alternates": "",
+ "name": "orthodox cross",
+ "shortname": ":orthodox_cross:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "christian",
+ "religion",
+ "symbol"
+ ]
+ },
"outbox_tray": {
"unicode": "1F4E4",
"unicode_alternates": [],
@@ -9449,7 +20031,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["email", "inbox"],
+ "keywords": [
+ "email",
+ "inbox"
+ ],
"moji": "📤"
},
"ox": {
@@ -9460,7 +20045,11 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "beef", "cow"],
+ "keywords": [
+ "animal",
+ "beef",
+ "cow"
+ ],
"moji": "🐂"
},
"package": {
@@ -9471,7 +20060,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["gift", "mail"],
+ "keywords": [
+ "gift",
+ "mail"
+ ],
"moji": "📦"
},
"page": {
@@ -9482,7 +20074,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["document"]
+ "keywords": [
+ "document"
+ ]
},
"page_facing_up": {
"unicode": "1F4C4",
@@ -9492,7 +20086,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents"],
+ "keywords": [
+ "documents"
+ ],
"moji": "📄"
},
"page_with_curl": {
@@ -9503,7 +20099,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents"],
+ "keywords": [
+ "documents"
+ ],
"moji": "📃"
},
"pager": {
@@ -9514,7 +20112,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bbcall", "oldschool"],
+ "keywords": [
+ "bbcall",
+ "oldschool"
+ ],
"moji": "📟"
},
"pages": {
@@ -9525,7 +20126,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents"]
+ "keywords": [
+ "documents"
+ ]
},
"paintbrush": {
"unicode": "1F58C",
@@ -9533,9 +20136,15 @@
"name": "lower left paintbrush",
"shortname": ":paintbrush:",
"category": "objects_symbols",
- "aliases": [":lower_left_paintbrush:"],
+ "aliases": [
+ ":lower_left_paintbrush:"
+ ],
"aliases_ascii": [],
- "keywords": ["brush", "art", "painting"]
+ "keywords": [
+ "brush",
+ "art",
+ "painting"
+ ]
},
"palm_tree": {
"unicode": "1F334",
@@ -9545,7 +20154,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "plant", "vegetable", "palm", "tree", "coconuts", "fronds", "warm", "tropical"],
+ "keywords": [
+ "nature",
+ "plant",
+ "vegetable",
+ "palm",
+ "tree",
+ "coconuts",
+ "fronds",
+ "warm",
+ "tropical"
+ ],
"moji": "🌴"
},
"panda_face": {
@@ -9556,7 +20175,22 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "panda", "bear", "face", "cub", "cute", "endearment", "friendship", "love", "bamboo", "china", "black", "white"],
+ "keywords": [
+ "animal",
+ "nature",
+ "panda",
+ "bear",
+ "face",
+ "cub",
+ "cute",
+ "endearment",
+ "friendship",
+ "love",
+ "bamboo",
+ "china",
+ "black",
+ "white"
+ ],
"moji": "🐼"
},
"paperclip": {
@@ -9567,7 +20201,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents", "stationery"],
+ "keywords": [
+ "documents",
+ "stationery"
+ ],
"moji": "📎"
},
"paperclips": {
@@ -9576,9 +20213,14 @@
"name": "linked paperclips",
"shortname": ":paperclips:",
"category": "objects_symbols",
- "aliases": [":linked_paperclips:"],
+ "aliases": [
+ ":linked_paperclips:"
+ ],
"aliases_ascii": [],
- "keywords": ["documents", "stationery"]
+ "keywords": [
+ "documents",
+ "stationery"
+ ]
},
"park": {
"unicode": "1F3DE",
@@ -9586,41 +20228,77 @@
"name": "national park",
"shortname": ":park:",
"category": "travel_places",
- "aliases": [":national_park:"],
- "aliases_ascii": [],
- "keywords": ["woods", "nature", "wildlife", "forest", "wilderness", "national"]
+ "aliases": [
+ ":national_park:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "woods",
+ "nature",
+ "wildlife",
+ "forest",
+ "wilderness",
+ "national"
+ ]
},
"parking": {
"unicode": "1F17F",
- "unicode_alternates": ["1F17F-FE0F"],
+ "unicode_alternates": [
+ "1F17F-FE0F"
+ ],
"name": "negative squared latin capital letter p",
"shortname": ":parking:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "blue-square", "cars", "letter"],
+ "keywords": [
+ "alphabet",
+ "blue-square",
+ "cars",
+ "letter"
+ ],
"moji": "🅿"
},
"part_alternation_mark": {
"unicode": "303D",
- "unicode_alternates": ["303D-FE0F"],
+ "unicode_alternates": [
+ "303D-FE0F"
+ ],
"name": "part alternation mark",
"shortname": ":part_alternation_mark:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["graph", "sing", "song", "vocal", "music", "karaoke", "cue", "letter", "m", "japanese"],
+ "keywords": [
+ "graph",
+ "sing",
+ "song",
+ "vocal",
+ "music",
+ "karaoke",
+ "cue",
+ "letter",
+ "m",
+ "japanese"
+ ],
"moji": "〽"
},
"partly_sunny": {
"unicode": "26C5",
- "unicode_alternates": ["26C5-FE0F"],
+ "unicode_alternates": [
+ "26C5-FE0F"
+ ],
"name": "sun behind cloud",
"shortname": ":partly_sunny:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cloud", "morning", "nature", "weather"],
+ "keywords": [
+ "cloud",
+ "morning",
+ "nature",
+ "weather"
+ ],
"moji": "⛅"
},
"passport_control": {
@@ -9631,9 +20309,48 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "custom", "passport", "official", "travel", "control", "foreign", "identification"],
+ "keywords": [
+ "blue-square",
+ "custom",
+ "passport",
+ "official",
+ "travel",
+ "control",
+ "foreign",
+ "identification"
+ ],
"moji": "🛂"
},
+ "pause_button": {
+ "unicode": "23F8",
+ "unicode_alternates": "",
+ "name": "double vertical bar",
+ "shortname": ":pause_button:",
+ "category": "symbols",
+ "aliases": [
+ ":double_vertical_bar:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "pause",
+ "sound",
+ "symbol"
+ ]
+ },
+ "peace": {
+ "unicode": "262E",
+ "unicode_alternates": "",
+ "name": "peace symbol",
+ "shortname": ":peace:",
+ "category": "symbols",
+ "aliases": [
+ ":peace_symbol:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "sign"
+ ]
+ },
"peach": {
"unicode": "1F351",
"unicode_alternates": [],
@@ -9642,7 +20359,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "nature", "peach", "fruit", "juicy", "pit"],
+ "keywords": [
+ "food",
+ "fruit",
+ "nature",
+ "peach",
+ "fruit",
+ "juicy",
+ "pit"
+ ],
"moji": "🍑"
},
"pear": {
@@ -9653,7 +20378,13 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fruit", "nature", "pear", "fruit", "shape"],
+ "keywords": [
+ "fruit",
+ "nature",
+ "pear",
+ "fruit",
+ "shape"
+ ],
"moji": "🍐"
},
"pen_ballpoint": {
@@ -9662,9 +20393,15 @@
"name": "lower left ballpoint pen",
"shortname": ":pen_ballpoint:",
"category": "objects_symbols",
- "aliases": [":lower_left_ballpoint_pen:"],
+ "aliases": [
+ ":lower_left_ballpoint_pen:"
+ ],
"aliases_ascii": [],
- "keywords": ["write", "bic", "ink"]
+ "keywords": [
+ "write",
+ "bic",
+ "ink"
+ ]
},
"pen_fountain": {
"unicode": "1F58B",
@@ -9672,9 +20409,15 @@
"name": "lower left fountain pen",
"shortname": ":pen_fountain:",
"category": "objects_symbols",
- "aliases": [":lower_left_fountain_pen:"],
+ "aliases": [
+ ":lower_left_fountain_pen:"
+ ],
"aliases_ascii": [],
- "keywords": ["write", "calligraphy", "ink"]
+ "keywords": [
+ "write",
+ "calligraphy",
+ "ink"
+ ]
},
"pencil": {
"unicode": "1F4DD",
@@ -9682,20 +20425,33 @@
"name": "memo",
"shortname": ":pencil:",
"category": "objects",
- "aliases": [":memo:"],
- "aliases_ascii": [],
- "keywords": ["documents", "paper", "station", "write"],
+ "aliases": [
+ ":memo:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "documents",
+ "paper",
+ "station",
+ "write"
+ ],
"moji": "📝"
},
"pencil2": {
"unicode": "270F",
- "unicode_alternates": ["270F-FE0F"],
+ "unicode_alternates": [
+ "270F-FE0F"
+ ],
"name": "pencil",
"shortname": ":pencil2:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["paper", "stationery", "write"],
+ "keywords": [
+ "paper",
+ "stationery",
+ "write"
+ ],
"moji": "✏"
},
"pencil3": {
@@ -9704,9 +20460,15 @@
"name": "lower left pencil",
"shortname": ":pencil3:",
"category": "objects_symbols",
- "aliases": [":lower_left_pencil:"],
+ "aliases": [
+ ":lower_left_pencil:"
+ ],
"aliases_ascii": [],
- "keywords": ["paper", "stationery", "write"]
+ "keywords": [
+ "paper",
+ "stationery",
+ "write"
+ ]
},
"penguin": {
"unicode": "1F427",
@@ -9716,7 +20478,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐧"
},
"pennant_black": {
@@ -9725,9 +20490,14 @@
"name": "black pennant",
"shortname": ":pennant_black:",
"category": "objects_symbols",
- "aliases": [":black_pennant:"],
+ "aliases": [
+ ":black_pennant:"
+ ],
"aliases_ascii": [],
- "keywords": ["flag", "athletics"]
+ "keywords": [
+ "flag",
+ "athletics"
+ ]
},
"pennant_white": {
"unicode": "1F3F1",
@@ -9735,9 +20505,14 @@
"name": "white pennant",
"shortname": ":pennant_white:",
"category": "objects_symbols",
- "aliases": [":white_pennant:"],
+ "aliases": [
+ ":white_pennant:"
+ ],
"aliases_ascii": [],
- "keywords": ["flag", "athletics"]
+ "keywords": [
+ "flag",
+ "athletics"
+ ]
},
"pensive": {
"unicode": "1F614",
@@ -9747,7 +20522,18 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "okay", "sad", "pensive", "thoughtful", "think", "reflective", "wistful", "meditate", "serious"],
+ "keywords": [
+ "face",
+ "okay",
+ "sad",
+ "pensive",
+ "thoughtful",
+ "think",
+ "reflective",
+ "wistful",
+ "meditate",
+ "serious"
+ ],
"moji": "😔"
},
"performing_arts": {
@@ -9758,7 +20544,19 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["acting", "drama", "theater", "performing", "arts", "performance", "entertainment", "acting", "story", "mask", "masks"],
+ "keywords": [
+ "acting",
+ "drama",
+ "theater",
+ "performing",
+ "arts",
+ "performance",
+ "entertainment",
+ "acting",
+ "story",
+ "mask",
+ "masks"
+ ],
"moji": "🎭"
},
"persevere": {
@@ -9768,8 +20566,17 @@
"shortname": ":persevere:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [">.<"],
- "keywords": ["endure", "persevere", "face", "no", "sick", "upset"],
+ "aliases_ascii": [
+ ">.<"
+ ],
+ "keywords": [
+ "endure",
+ "persevere",
+ "face",
+ "no",
+ "sick",
+ "upset"
+ ],
"moji": "😣"
},
"person_frowning": {
@@ -9780,9 +20587,107 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girl", "woman", "dejected", "rejected", "sad", "frown"],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "dejected",
+ "rejected",
+ "sad",
+ "frown"
+ ],
"moji": "🙍"
},
+ "person_frowning_tone1": {
+ "unicode": "1F64D-1F3FB",
+ "unicode_alternates": "",
+ "name": "person frowning tone 1",
+ "shortname": ":person_frowning_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "dejected",
+ "rejected",
+ "sad",
+ "frown"
+ ]
+ },
+ "person_frowning_tone2": {
+ "unicode": "1F64D-1F3FC",
+ "unicode_alternates": "",
+ "name": "person frowning tone 2",
+ "shortname": ":person_frowning_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "dejected",
+ "rejected",
+ "sad",
+ "frown"
+ ]
+ },
+ "person_frowning_tone3": {
+ "unicode": "1F64D-1F3FD",
+ "unicode_alternates": "",
+ "name": "person frowning tone 3",
+ "shortname": ":person_frowning_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "dejected",
+ "rejected",
+ "sad",
+ "frown"
+ ]
+ },
+ "person_frowning_tone4": {
+ "unicode": "1F64D-1F3FE",
+ "unicode_alternates": "",
+ "name": "person frowning tone 4",
+ "shortname": ":person_frowning_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "dejected",
+ "rejected",
+ "sad",
+ "frown"
+ ]
+ },
+ "person_frowning_tone5": {
+ "unicode": "1F64D-1F3FF",
+ "unicode_alternates": "",
+ "name": "person frowning tone 5",
+ "shortname": ":person_frowning_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "dejected",
+ "rejected",
+ "sad",
+ "frown"
+ ]
+ },
"person_with_blond_hair": {
"unicode": "1F471",
"unicode_alternates": [],
@@ -9791,9 +20696,107 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["male", "man", "blonde", "young", "western", "westerner", "occidental"],
+ "keywords": [
+ "male",
+ "man",
+ "blonde",
+ "young",
+ "western",
+ "westerner",
+ "occidental"
+ ],
"moji": "👱"
},
+ "person_with_blond_hair_tone1": {
+ "unicode": "1F471-1F3FB",
+ "unicode_alternates": "",
+ "name": "person with blond hair tone 1",
+ "shortname": ":person_with_blond_hair_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "man",
+ "blonde",
+ "young",
+ "western",
+ "westerner",
+ "occidental"
+ ]
+ },
+ "person_with_blond_hair_tone2": {
+ "unicode": "1F471-1F3FC",
+ "unicode_alternates": "",
+ "name": "person with blond hair tone 2",
+ "shortname": ":person_with_blond_hair_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "man",
+ "blonde",
+ "young",
+ "western",
+ "westerner",
+ "occidental"
+ ]
+ },
+ "person_with_blond_hair_tone3": {
+ "unicode": "1F471-1F3FD",
+ "unicode_alternates": "",
+ "name": "person with blond hair tone 3",
+ "shortname": ":person_with_blond_hair_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "man",
+ "blonde",
+ "young",
+ "western",
+ "westerner",
+ "occidental"
+ ]
+ },
+ "person_with_blond_hair_tone4": {
+ "unicode": "1F471-1F3FE",
+ "unicode_alternates": "",
+ "name": "person with blond hair tone 4",
+ "shortname": ":person_with_blond_hair_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "man",
+ "blonde",
+ "young",
+ "western",
+ "westerner",
+ "occidental"
+ ]
+ },
+ "person_with_blond_hair_tone5": {
+ "unicode": "1F471-1F3FF",
+ "unicode_alternates": "",
+ "name": "person with blond hair tone 5",
+ "shortname": ":person_with_blond_hair_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "male",
+ "man",
+ "blonde",
+ "young",
+ "western",
+ "westerner",
+ "occidental"
+ ]
+ },
"person_with_pouting_face": {
"unicode": "1F64E",
"unicode_alternates": [],
@@ -9802,9 +20805,121 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girl", "woman", "pout", "sexy", "cute", "annoyed"],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "pout",
+ "sexy",
+ "cute",
+ "annoyed"
+ ],
"moji": "🙎"
},
+ "person_with_pouting_face_tone1": {
+ "unicode": "1F64E-1F3FB",
+ "unicode_alternates": "",
+ "name": "person with pouting face tone1",
+ "shortname": ":person_with_pouting_face_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "pout",
+ "sexy",
+ "cute",
+ "annoyed"
+ ]
+ },
+ "person_with_pouting_face_tone2": {
+ "unicode": "1F64E-1F3FC",
+ "unicode_alternates": "",
+ "name": "person with pouting face tone2",
+ "shortname": ":person_with_pouting_face_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "pout",
+ "sexy",
+ "cute",
+ "annoyed"
+ ]
+ },
+ "person_with_pouting_face_tone3": {
+ "unicode": "1F64E-1F3FD",
+ "unicode_alternates": "",
+ "name": "person with pouting face tone3",
+ "shortname": ":person_with_pouting_face_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "pout",
+ "sexy",
+ "cute",
+ "annoyed"
+ ]
+ },
+ "person_with_pouting_face_tone4": {
+ "unicode": "1F64E-1F3FE",
+ "unicode_alternates": "",
+ "name": "person with pouting face tone4",
+ "shortname": ":person_with_pouting_face_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "pout",
+ "sexy",
+ "cute",
+ "annoyed"
+ ]
+ },
+ "person_with_pouting_face_tone5": {
+ "unicode": "1F64E-1F3FF",
+ "unicode_alternates": "",
+ "name": "person with pouting face tone5",
+ "shortname": ":person_with_pouting_face_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "pout",
+ "sexy",
+ "cute",
+ "annoyed"
+ ]
+ },
+ "pick": {
+ "unicode": "26CF",
+ "unicode_alternates": "",
+ "name": "pick",
+ "shortname": ":pick:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "mining",
+ "object",
+ "tool"
+ ]
+ },
"pig": {
"unicode": "1F437",
"unicode_alternates": [],
@@ -9813,7 +20928,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "oink"],
+ "keywords": [
+ "animal",
+ "oink"
+ ],
"moji": "🐷"
},
"pig2": {
@@ -9824,7 +20942,21 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "pig", "piggy", "pork", "ham", "hog", "bacon", "oink", "slop", "livestock", "greed", "greedy"],
+ "keywords": [
+ "animal",
+ "nature",
+ "pig",
+ "piggy",
+ "pork",
+ "ham",
+ "hog",
+ "bacon",
+ "oink",
+ "slop",
+ "livestock",
+ "greed",
+ "greedy"
+ ],
"moji": "🐖"
},
"pig_nose": {
@@ -9835,7 +20967,20 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "oink", "pig", "nose", "snout", "food", "eat", "cute", "oink", "pink", "smell", "truffle"],
+ "keywords": [
+ "animal",
+ "oink",
+ "pig",
+ "nose",
+ "snout",
+ "food",
+ "eat",
+ "cute",
+ "oink",
+ "pink",
+ "smell",
+ "truffle"
+ ],
"moji": "🐽"
},
"pill": {
@@ -9846,7 +20991,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["health", "medicine"],
+ "keywords": [
+ "health",
+ "medicine"
+ ],
"moji": "💊"
},
"pineapple": {
@@ -9857,28 +21005,68 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "nature", "pineapple", "pina", "tropical", "flower"],
+ "keywords": [
+ "food",
+ "fruit",
+ "nature",
+ "pineapple",
+ "pina",
+ "tropical",
+ "flower"
+ ],
"moji": "🍍"
},
+ "ping_pong": {
+ "unicode": "1F3D3",
+ "unicode_alternates": "",
+ "name": "table tennis paddle and ball",
+ "shortname": ":ping_pong:",
+ "category": "activity",
+ "aliases": [
+ ":table_tennis:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"piracy": {
"unicode": "1F572",
"unicode_alternates": [],
"name": "no piracy",
"shortname": ":piracy:",
"category": "objects_symbols",
- "aliases": [":no_piracy:"],
+ "aliases": [
+ ":no_piracy:"
+ ],
"aliases_ascii": [],
- "keywords": ["theft", "rule"]
+ "keywords": [
+ "theft",
+ "rule"
+ ]
},
"pisces": {
"unicode": "2653",
- "unicode_alternates": ["2653-FE0F"],
+ "unicode_alternates": [
+ "2653-FE0F"
+ ],
"name": "pisces",
"shortname": ":pisces:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["pisces", "fish", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "pisces",
+ "fish",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "purple-square",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♓"
},
"pizza": {
@@ -9889,9 +21077,48 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "party", "pizza", "pie", "new york", "italian", "italy", "slice", "peperoni"],
+ "keywords": [
+ "food",
+ "party",
+ "pizza",
+ "pie",
+ "new york",
+ "italian",
+ "italy",
+ "slice",
+ "peperoni"
+ ],
"moji": "🍕"
},
+ "place_of_worship": {
+ "unicode": "1F6D0",
+ "unicode_alternates": "",
+ "name": "place of worship",
+ "shortname": ":place_of_worship:",
+ "category": "symbols",
+ "aliases": [
+ ":worship_symbol:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "play_pause": {
+ "unicode": "23EF",
+ "unicode_alternates": "",
+ "name": "black right-pointing double triangle with double vertical bar",
+ "shortname": ":play_pause:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "arrow",
+ "pause",
+ "play",
+ "right",
+ "sound",
+ "symbol"
+ ]
+ },
"point_down": {
"unicode": "1F447",
"unicode_alternates": [],
@@ -9900,9 +21127,83 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["direction", "fingers", "hand"],
+ "keywords": [
+ "direction",
+ "fingers",
+ "hand"
+ ],
"moji": "👇"
},
+ "point_down_tone1": {
+ "unicode": "1F447-1F3FB",
+ "unicode_alternates": "",
+ "name": "white down pointing backhand index tone 1",
+ "shortname": ":point_down_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_down_tone2": {
+ "unicode": "1F447-1F3FC",
+ "unicode_alternates": "",
+ "name": "white down pointing backhand index tone 2",
+ "shortname": ":point_down_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_down_tone3": {
+ "unicode": "1F447-1F3FD",
+ "unicode_alternates": "",
+ "name": "white down pointing backhand index tone 3",
+ "shortname": ":point_down_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_down_tone4": {
+ "unicode": "1F447-1F3FE",
+ "unicode_alternates": "",
+ "name": "white down pointing backhand index tone 4",
+ "shortname": ":point_down_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_down_tone5": {
+ "unicode": "1F447-1F3FF",
+ "unicode_alternates": "",
+ "name": "white down pointing backhand index tone 5",
+ "shortname": ":point_down_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
"point_left": {
"unicode": "1F448",
"unicode_alternates": [],
@@ -9911,9 +21212,83 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["direction", "fingers", "hand"],
+ "keywords": [
+ "direction",
+ "fingers",
+ "hand"
+ ],
"moji": "👈"
},
+ "point_left_tone1": {
+ "unicode": "1F448-1F3FB",
+ "unicode_alternates": "",
+ "name": "white left pointing backhand index tone 1",
+ "shortname": ":point_left_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_left_tone2": {
+ "unicode": "1F448-1F3FC",
+ "unicode_alternates": "",
+ "name": "white left pointing backhand index tone 2",
+ "shortname": ":point_left_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_left_tone3": {
+ "unicode": "1F448-1F3FD",
+ "unicode_alternates": "",
+ "name": "white left pointing backhand index tone 3",
+ "shortname": ":point_left_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_left_tone4": {
+ "unicode": "1F448-1F3FE",
+ "unicode_alternates": "",
+ "name": "white left pointing backhand index tone 4",
+ "shortname": ":point_left_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_left_tone5": {
+ "unicode": "1F448-1F3FF",
+ "unicode_alternates": "",
+ "name": "white left pointing backhand index tone 5",
+ "shortname": ":point_left_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
"point_right": {
"unicode": "1F449",
"unicode_alternates": [],
@@ -9922,18 +21297,98 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["direction", "fingers", "hand"],
+ "keywords": [
+ "direction",
+ "fingers",
+ "hand"
+ ],
"moji": "👉"
},
+ "point_right_tone1": {
+ "unicode": "1F449-1F3FB",
+ "unicode_alternates": "",
+ "name": "white right pointing backhand index tone 1",
+ "shortname": ":point_right_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_right_tone2": {
+ "unicode": "1F449-1F3FC",
+ "unicode_alternates": "",
+ "name": "white right pointing backhand index tone 2",
+ "shortname": ":point_right_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_right_tone3": {
+ "unicode": "1F449-1F3FD",
+ "unicode_alternates": "",
+ "name": "white right pointing backhand index tone 3",
+ "shortname": ":point_right_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_right_tone4": {
+ "unicode": "1F449-1F3FE",
+ "unicode_alternates": "",
+ "name": "white right pointing backhand index tone 4",
+ "shortname": ":point_right_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
+ "point_right_tone5": {
+ "unicode": "1F449-1F3FF",
+ "unicode_alternates": "",
+ "name": "white right pointing backhand index tone 5",
+ "shortname": ":point_right_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand"
+ ]
+ },
"point_up": {
"unicode": "261D",
- "unicode_alternates": ["261D-FE0F"],
+ "unicode_alternates": [
+ "261D-FE0F"
+ ],
"name": "white up pointing index",
"shortname": ":point_up:",
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["direction", "fingers", "hand"],
+ "keywords": [
+ "direction",
+ "fingers",
+ "hand"
+ ],
"moji": "☝"
},
"point_up_2": {
@@ -9944,9 +21399,163 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["direction", "fingers", "hand"],
+ "keywords": [
+ "direction",
+ "fingers",
+ "hand"
+ ],
"moji": "👆"
},
+ "point_up_2_tone1": {
+ "unicode": "1F446-1F3FB",
+ "unicode_alternates": "",
+ "name": "white up pointing backhand index tone 1",
+ "shortname": ":point_up_2_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_2_tone2": {
+ "unicode": "1F446-1F3FC",
+ "unicode_alternates": "",
+ "name": "white up pointing backhand index tone 2",
+ "shortname": ":point_up_2_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_2_tone3": {
+ "unicode": "1F446-1F3FD",
+ "unicode_alternates": "",
+ "name": "white up pointing backhand index tone 3",
+ "shortname": ":point_up_2_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_2_tone4": {
+ "unicode": "1F446-1F3FE",
+ "unicode_alternates": "",
+ "name": "white up pointing backhand index tone 4",
+ "shortname": ":point_up_2_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_2_tone5": {
+ "unicode": "1F446-1F3FF",
+ "unicode_alternates": "",
+ "name": "white up pointing backhand index tone 5",
+ "shortname": ":point_up_2_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_tone1": {
+ "unicode": "261D-1F3FB",
+ "unicode_alternates": "",
+ "name": "white up pointing index tone 1",
+ "shortname": ":point_up_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_tone2": {
+ "unicode": "261D-1F3FC",
+ "unicode_alternates": "",
+ "name": "white up pointing index tone 2",
+ "shortname": ":point_up_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_tone3": {
+ "unicode": "261D-1F3FD",
+ "unicode_alternates": "",
+ "name": "white up pointing index tone 3",
+ "shortname": ":point_up_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_tone4": {
+ "unicode": "261D-1F3FE",
+ "unicode_alternates": "",
+ "name": "white up pointing index tone 4",
+ "shortname": ":point_up_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
+ "point_up_tone5": {
+ "unicode": "261D-1F3FF",
+ "unicode_alternates": "",
+ "name": "white up pointing index tone 5",
+ "shortname": ":point_up_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "direction",
+ "finger",
+ "hand",
+ "one"
+ ]
+ },
"police_car": {
"unicode": "1F693",
"unicode_alternates": [],
@@ -9955,7 +21564,21 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cars", "enforcement", "law", "transportation", "vehicle", "police", "car", "emergency", "ticket", "citation", "crime", "help", "officer"],
+ "keywords": [
+ "cars",
+ "enforcement",
+ "law",
+ "transportation",
+ "vehicle",
+ "police",
+ "car",
+ "emergency",
+ "ticket",
+ "citation",
+ "crime",
+ "help",
+ "officer"
+ ],
"moji": "🚓"
},
"poodle": {
@@ -9966,7 +21589,18 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["101", "animal", "dog", "nature", "poodle", "dog", "clip", "showy", "sophisticated", "vain"],
+ "keywords": [
+ "101",
+ "animal",
+ "dog",
+ "nature",
+ "poodle",
+ "dog",
+ "clip",
+ "showy",
+ "sophisticated",
+ "vain"
+ ],
"moji": "🐩"
},
"poop": {
@@ -9975,11 +21609,31 @@
"name": "pile of poo",
"shortname": ":poop:",
"category": "emoticons",
- "aliases": [":shit:", ":hankey:", ":poo:"],
- "aliases_ascii": [],
- "keywords": ["poop", "shit", "shitface", "turd", "poo"],
+ "aliases": [
+ ":shit:",
+ ":hankey:",
+ ":poo:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "poop",
+ "shit",
+ "shitface",
+ "turd",
+ "poo"
+ ],
"moji": "💩"
},
+ "popcorn": {
+ "unicode": "1F37F",
+ "unicode_alternates": "",
+ "name": "popcorn",
+ "shortname": ":popcorn:",
+ "category": "foods",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"post_office": {
"unicode": "1F3E3",
"unicode_alternates": [],
@@ -9988,7 +21642,11 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "communication", "email"],
+ "keywords": [
+ "building",
+ "communication",
+ "email"
+ ],
"moji": "🏣"
},
"postal_horn": {
@@ -9999,7 +21657,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["instrument", "music"],
+ "keywords": [
+ "instrument",
+ "music"
+ ],
"moji": "📯"
},
"postbox": {
@@ -10010,7 +21671,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["email", "envelope", "letter"],
+ "keywords": [
+ "email",
+ "envelope",
+ "letter"
+ ],
"moji": "📮"
},
"potable_water": {
@@ -10021,7 +21686,21 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "cleaning", "faucet", "liquid", "restroom", "potable", "water", "drinkable", "pure", "clear", "clean", "aqua", "h20"],
+ "keywords": [
+ "blue-square",
+ "cleaning",
+ "faucet",
+ "liquid",
+ "restroom",
+ "potable",
+ "water",
+ "drinkable",
+ "pure",
+ "clear",
+ "clean",
+ "aqua",
+ "h20"
+ ],
"moji": "🚰"
},
"pouch": {
@@ -10032,7 +21711,16 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["accessories", "bag", "pouch", "bag", "cosmetic", "packing", "grandma", "makeup"],
+ "keywords": [
+ "accessories",
+ "bag",
+ "pouch",
+ "bag",
+ "cosmetic",
+ "packing",
+ "grandma",
+ "makeup"
+ ],
"moji": "👝"
},
"poultry_leg": {
@@ -10043,7 +21731,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "meat", "poultry", "leg", "chicken", "fried"],
+ "keywords": [
+ "food",
+ "meat",
+ "poultry",
+ "leg",
+ "chicken",
+ "fried"
+ ],
"moji": "🍗"
},
"pound": {
@@ -10054,7 +21749,24 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bills", "british", "currency", "england", "money", "sterling", "uk", "pound", "britain", "british", "banknote", "money", "currency", "paper", "cash", "bills"],
+ "keywords": [
+ "bills",
+ "british",
+ "currency",
+ "england",
+ "money",
+ "sterling",
+ "uk",
+ "pound",
+ "britain",
+ "british",
+ "banknote",
+ "money",
+ "currency",
+ "paper",
+ "cash",
+ "bills"
+ ],
"moji": "💷"
},
"pouting_cat": {
@@ -10065,7 +21777,15 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cats", "pout", "annoyed", "miffed", "glower", "frown"],
+ "keywords": [
+ "animal",
+ "cats",
+ "pout",
+ "annoyed",
+ "miffed",
+ "glower",
+ "frown"
+ ],
"moji": "😾"
},
"pray": {
@@ -10076,9 +21796,136 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["highfive", "hope", "namaste", "please", "wish", "pray", "high five", "hands", "sorrow", "regret", "sorry"],
+ "keywords": [
+ "highfive",
+ "hope",
+ "namaste",
+ "please",
+ "wish",
+ "pray",
+ "high five",
+ "hands",
+ "sorrow",
+ "regret",
+ "sorry"
+ ],
"moji": "🙏"
},
+ "pray_tone1": {
+ "unicode": "1F64F-1F3FB",
+ "unicode_alternates": "",
+ "name": "person with folded hands tone 1",
+ "shortname": ":pray_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "highfive",
+ "hope",
+ "namaste",
+ "please",
+ "wish",
+ "pray",
+ "high five",
+ "sorrow",
+ "regret",
+ "sorry"
+ ]
+ },
+ "pray_tone2": {
+ "unicode": "1F64F-1F3FC",
+ "unicode_alternates": "",
+ "name": "person with folded hands tone 2",
+ "shortname": ":pray_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "highfive",
+ "hope",
+ "namaste",
+ "please",
+ "wish",
+ "pray",
+ "high five",
+ "sorrow",
+ "regret",
+ "sorry"
+ ]
+ },
+ "pray_tone3": {
+ "unicode": "1F64F-1F3FD",
+ "unicode_alternates": "",
+ "name": "person with folded hands tone 3",
+ "shortname": ":pray_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "highfive",
+ "hope",
+ "namaste",
+ "please",
+ "wish",
+ "pray",
+ "high five",
+ "sorrow",
+ "regret",
+ "sorry"
+ ]
+ },
+ "pray_tone4": {
+ "unicode": "1F64F-1F3FE",
+ "unicode_alternates": "",
+ "name": "person with folded hands tone 4",
+ "shortname": ":pray_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "highfive",
+ "hope",
+ "namaste",
+ "please",
+ "wish",
+ "pray",
+ "high five",
+ "sorrow",
+ "regret",
+ "sorry"
+ ]
+ },
+ "pray_tone5": {
+ "unicode": "1F64F-1F3FF",
+ "unicode_alternates": "",
+ "name": "person with folded hands tone 5",
+ "shortname": ":pray_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "highfive",
+ "hope",
+ "namaste",
+ "please",
+ "wish",
+ "pray",
+ "high five",
+ "sorrow",
+ "regret",
+ "sorry"
+ ]
+ },
+ "prayer_beads": {
+ "unicode": "1F4FF",
+ "unicode_alternates": "",
+ "name": "prayer beads",
+ "shortname": ":prayer_beads:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"princess": {
"unicode": "1F478",
"unicode_alternates": [],
@@ -10087,9 +21934,138 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blond", "crown", "female", "girl", "woman", "princess", "royal", "royalty", "king", "queen", "daughter", "disney", "high-maintenance"],
+ "keywords": [
+ "blond",
+ "crown",
+ "female",
+ "girl",
+ "woman",
+ "princess",
+ "royal",
+ "royalty",
+ "king",
+ "queen",
+ "daughter",
+ "disney",
+ "high-maintenance"
+ ],
"moji": "👸"
},
+ "princess_tone1": {
+ "unicode": "1F478-1F3FB",
+ "unicode_alternates": "",
+ "name": "princess tone 1",
+ "shortname": ":princess_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "blond",
+ "crown",
+ "female",
+ "girl",
+ "woman",
+ "royal",
+ "royalty",
+ "king",
+ "queen",
+ "daughter",
+ "disney",
+ "high-maintenance"
+ ]
+ },
+ "princess_tone2": {
+ "unicode": "1F478-1F3FC",
+ "unicode_alternates": "",
+ "name": "princess tone 2",
+ "shortname": ":princess_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "blond",
+ "crown",
+ "female",
+ "girl",
+ "woman",
+ "royal",
+ "royalty",
+ "king",
+ "queen",
+ "daughter",
+ "disney",
+ "high-maintenance"
+ ]
+ },
+ "princess_tone3": {
+ "unicode": "1F478-1F3FD",
+ "unicode_alternates": "",
+ "name": "princess tone 3",
+ "shortname": ":princess_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "blond",
+ "crown",
+ "female",
+ "girl",
+ "woman",
+ "royal",
+ "royalty",
+ "king",
+ "queen",
+ "daughter",
+ "disney",
+ "high-maintenance"
+ ]
+ },
+ "princess_tone4": {
+ "unicode": "1F478-1F3FE",
+ "unicode_alternates": "",
+ "name": "princess tone 4",
+ "shortname": ":princess_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "blond",
+ "crown",
+ "female",
+ "girl",
+ "woman",
+ "royal",
+ "royalty",
+ "king",
+ "queen",
+ "daughter",
+ "disney",
+ "high-maintenance"
+ ]
+ },
+ "princess_tone5": {
+ "unicode": "1F478-1F3FF",
+ "unicode_alternates": "",
+ "name": "princess tone 5",
+ "shortname": ":princess_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "blond",
+ "crown",
+ "female",
+ "girl",
+ "woman",
+ "royal",
+ "royalty",
+ "king",
+ "queen",
+ "daughter",
+ "disney",
+ "high-maintenance"
+ ]
+ },
"printer": {
"unicode": "1F5A8",
"unicode_alternates": [],
@@ -10098,7 +22074,12 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["hardcopy", "paper", "inkjet", "laser"]
+ "keywords": [
+ "hardcopy",
+ "paper",
+ "inkjet",
+ "laser"
+ ]
},
"prohibited": {
"unicode": "1F6C7",
@@ -10106,9 +22087,19 @@
"name": "prohibited sign",
"shortname": ":prohibited:",
"category": "objects_symbols",
- "aliases": [":prohibited_sign:"],
- "aliases_ascii": [],
- "keywords": ["no", "not", "denied", "disallow", "forbid", "limit", "stop"]
+ "aliases": [
+ ":prohibited_sign:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "no",
+ "not",
+ "denied",
+ "disallow",
+ "forbid",
+ "limit",
+ "stop"
+ ]
},
"projector": {
"unicode": "1F4FD",
@@ -10116,9 +22107,18 @@
"name": "film projector",
"shortname": ":projector:",
"category": "objects_symbols",
- "aliases": [":film_projector:"],
- "aliases_ascii": [],
- "keywords": ["movie", "video", "motion", "picture", "8mm", "16mm"]
+ "aliases": [
+ ":film_projector:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "movie",
+ "video",
+ "motion",
+ "picture",
+ "8mm",
+ "16mm"
+ ]
},
"punch": {
"unicode": "1F44A",
@@ -10128,9 +22128,77 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fist", "hand"],
+ "keywords": [
+ "fist",
+ "hand"
+ ],
"moji": "👊"
},
+ "punch_tone1": {
+ "unicode": "1F44A-1F3FB",
+ "unicode_alternates": "",
+ "name": "fisted hand sign tone 1",
+ "shortname": ":punch_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fist",
+ "punch"
+ ]
+ },
+ "punch_tone2": {
+ "unicode": "1F44A-1F3FC",
+ "unicode_alternates": "",
+ "name": "fisted hand sign tone 2",
+ "shortname": ":punch_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fist",
+ "punch"
+ ]
+ },
+ "punch_tone3": {
+ "unicode": "1F44A-1F3FD",
+ "unicode_alternates": "",
+ "name": "fisted hand sign tone 3",
+ "shortname": ":punch_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fist",
+ "punch"
+ ]
+ },
+ "punch_tone4": {
+ "unicode": "1F44A-1F3FE",
+ "unicode_alternates": "",
+ "name": "fisted hand sign tone 4",
+ "shortname": ":punch_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fist",
+ "punch"
+ ]
+ },
+ "punch_tone5": {
+ "unicode": "1F44A-1F3FF",
+ "unicode_alternates": "",
+ "name": "fisted hand sign tone 5",
+ "shortname": ":punch_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fist",
+ "punch"
+ ]
+ },
"purple_heart": {
"unicode": "1F49C",
"unicode_alternates": [],
@@ -10139,7 +22207,25 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines", "purple", "violet", "heart", "love", "sensitive", "understanding", "compassionate", "compassion", "duty", "honor", "royalty", "veteran", "sacrifice"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines",
+ "purple",
+ "violet",
+ "heart",
+ "love",
+ "sensitive",
+ "understanding",
+ "compassionate",
+ "compassion",
+ "duty",
+ "honor",
+ "royalty",
+ "veteran",
+ "sacrifice"
+ ],
"moji": "💜"
},
"purse": {
@@ -10150,7 +22236,20 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["accessories", "fashion", "money", "purse", "clutch", "bag", "handbag", "coin bag", "accessory", "money", "ladies", "shopping"],
+ "keywords": [
+ "accessories",
+ "fashion",
+ "money",
+ "purse",
+ "clutch",
+ "bag",
+ "handbag",
+ "coin bag",
+ "accessory",
+ "money",
+ "ladies",
+ "shopping"
+ ],
"moji": "👛"
},
"pushpin": {
@@ -10161,7 +22260,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["stationery"],
+ "keywords": [
+ "stationery"
+ ],
"moji": "📌"
},
"pushpin_black": {
@@ -10172,7 +22273,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["stationery"]
+ "keywords": [
+ "stationery"
+ ]
},
"put_litter_in_its_place": {
"unicode": "1F6AE",
@@ -10182,7 +22285,15 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "litter", "waste", "trash", "garbage", "receptacle", "can"],
+ "keywords": [
+ "blue-square",
+ "litter",
+ "waste",
+ "trash",
+ "garbage",
+ "receptacle",
+ "can"
+ ],
"moji": "🚮"
},
"question": {
@@ -10193,7 +22304,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["confused", "doubt"],
+ "keywords": [
+ "confused",
+ "doubt"
+ ],
"moji": "❓"
},
"rabbit": {
@@ -10204,7 +22318,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐰"
},
"rabbit2": {
@@ -10215,7 +22332,15 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "rabbit", "bunny", "easter", "reproduction", "prolific"],
+ "keywords": [
+ "animal",
+ "nature",
+ "rabbit",
+ "bunny",
+ "easter",
+ "reproduction",
+ "prolific"
+ ],
"moji": "🐇"
},
"race_car": {
@@ -10224,9 +22349,18 @@
"name": "racing car",
"shortname": ":race_car:",
"category": "activity",
- "aliases": [":racing_car:"],
- "aliases_ascii": [],
- "keywords": ["formula 1", "race", "stock", "nascar", "speed", "drive"]
+ "aliases": [
+ ":racing_car:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "formula 1",
+ "race",
+ "stock",
+ "nascar",
+ "speed",
+ "drive"
+ ]
},
"racehorse": {
"unicode": "1F40E",
@@ -10236,7 +22370,29 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "gamble", "horse", "powerful", "draft", "calvary", "cowboy", "cowgirl", "mounted", "race", "ride", "gallop", "trot", "colt", "filly", "mare", "stallion", "gelding", "yearling", "thoroughbred", "pony"],
+ "keywords": [
+ "animal",
+ "gamble",
+ "horse",
+ "powerful",
+ "draft",
+ "calvary",
+ "cowboy",
+ "cowgirl",
+ "mounted",
+ "race",
+ "ride",
+ "gallop",
+ "trot",
+ "colt",
+ "filly",
+ "mare",
+ "stallion",
+ "gelding",
+ "yearling",
+ "thoroughbred",
+ "pony"
+ ],
"moji": "🐎"
},
"radio": {
@@ -10247,7 +22403,12 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "music", "podcast", "program"],
+ "keywords": [
+ "communication",
+ "music",
+ "podcast",
+ "program"
+ ],
"moji": "📻"
},
"radio_button": {
@@ -10258,9 +22419,25 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["input"],
+ "keywords": [
+ "input"
+ ],
"moji": "🔘"
},
+ "radioactive": {
+ "unicode": "2622",
+ "unicode_alternates": "",
+ "name": "radioactive sign",
+ "shortname": ":radioactive:",
+ "category": "symbols",
+ "aliases": [
+ ":radioactive_sign:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "symbol"
+ ]
+ },
"rage": {
"unicode": "1F621",
"unicode_alternates": [],
@@ -10269,7 +22446,16 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["angry", "despise", "hate", "mad", "pout", "anger", "rage", "irate"],
+ "keywords": [
+ "angry",
+ "despise",
+ "hate",
+ "mad",
+ "pout",
+ "anger",
+ "rage",
+ "irate"
+ ],
"moji": "😡"
},
"railway_car": {
@@ -10280,7 +22466,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "railway", "rail", "car", "coach", "train"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "railway",
+ "rail",
+ "car",
+ "coach",
+ "train"
+ ],
"moji": "🚃"
},
"railway_track": {
@@ -10289,9 +22483,17 @@
"name": "railway track",
"shortname": ":railway_track:",
"category": "travel_places",
- "aliases": [":railroad_track:"],
- "aliases_ascii": [],
- "keywords": ["train", "trolley", "subway", "locomotive", "transit"]
+ "aliases": [
+ ":railroad_track:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "train",
+ "trolley",
+ "subway",
+ "locomotive",
+ "transit"
+ ]
},
"rainbow": {
"unicode": "1F308",
@@ -10301,7 +22503,21 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["happy", "nature", "photo", "sky", "unicorn", "rainbow", "color", "pride", "diversity", "spectrum", "refract", "leprechaun", "gold"],
+ "keywords": [
+ "happy",
+ "nature",
+ "photo",
+ "sky",
+ "unicorn",
+ "rainbow",
+ "color",
+ "pride",
+ "diversity",
+ "spectrum",
+ "refract",
+ "leprechaun",
+ "gold"
+ ],
"moji": "🌈"
},
"raised_hand": {
@@ -10312,9 +22528,83 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girl", "woman"],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ],
"moji": "✋"
},
+ "raised_hand_tone1": {
+ "unicode": "270B-1F3FB",
+ "unicode_alternates": "",
+ "name": "raised hand tone 1",
+ "shortname": ":raised_hand_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "raised_hand_tone2": {
+ "unicode": "270B-1F3FC",
+ "unicode_alternates": "",
+ "name": "raised hand tone 2",
+ "shortname": ":raised_hand_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "raised_hand_tone3": {
+ "unicode": "270B-1F3FD",
+ "unicode_alternates": "",
+ "name": "raised hand tone 3",
+ "shortname": ":raised_hand_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "raised_hand_tone4": {
+ "unicode": "270B-1F3FE",
+ "unicode_alternates": "",
+ "name": "raised hand tone 4",
+ "shortname": ":raised_hand_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
+ "raised_hand_tone5": {
+ "unicode": "270B-1F3FF",
+ "unicode_alternates": "",
+ "name": "raised hand tone 5",
+ "shortname": ":raised_hand_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman"
+ ]
+ },
"raised_hands": {
"unicode": "1F64C",
"unicode_alternates": [],
@@ -10323,9 +22613,106 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["gesture", "hooray", "winning", "woot", "yay", "banzai"],
+ "keywords": [
+ "gesture",
+ "hooray",
+ "winning",
+ "woot",
+ "yay",
+ "banzai"
+ ],
"moji": "🙌"
},
+ "raised_hands_tone1": {
+ "unicode": "1F64C-1F3FB",
+ "unicode_alternates": "",
+ "name": "person raising both hands in celebration tone 1",
+ "shortname": ":raised_hands_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "gesture",
+ "hooray",
+ "winning",
+ "woot",
+ "yay",
+ "banzai",
+ "raised"
+ ]
+ },
+ "raised_hands_tone2": {
+ "unicode": "1F64C-1F3FC",
+ "unicode_alternates": "",
+ "name": "person raising both hands in celebration tone 2",
+ "shortname": ":raised_hands_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "gesture",
+ "hooray",
+ "winning",
+ "woot",
+ "yay",
+ "banzai",
+ "raised"
+ ]
+ },
+ "raised_hands_tone3": {
+ "unicode": "1F64C-1F3FD",
+ "unicode_alternates": "",
+ "name": "person raising both hands in celebration tone 3",
+ "shortname": ":raised_hands_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "gesture",
+ "hooray",
+ "winning",
+ "woot",
+ "yay",
+ "banzai",
+ "raised"
+ ]
+ },
+ "raised_hands_tone4": {
+ "unicode": "1F64C-1F3FE",
+ "unicode_alternates": "",
+ "name": "person raising both hands in celebration tone 4",
+ "shortname": ":raised_hands_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "gesture",
+ "hooray",
+ "winning",
+ "woot",
+ "yay",
+ "banzai",
+ "raised"
+ ]
+ },
+ "raised_hands_tone5": {
+ "unicode": "1F64C-1F3FF",
+ "unicode_alternates": "",
+ "name": "person raising both hands in celebration tone 5",
+ "shortname": ":raised_hands_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "gesture",
+ "hooray",
+ "winning",
+ "woot",
+ "yay",
+ "banzai",
+ "raised"
+ ]
+ },
"raising_hand": {
"unicode": "1F64B",
"unicode_alternates": [],
@@ -10334,9 +22721,108 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girl", "woman", "hand", "raise", "notice", "attention", "answer"],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "hand",
+ "raise",
+ "notice",
+ "attention",
+ "answer"
+ ],
"moji": "🙋"
},
+ "raising_hand_tone1": {
+ "unicode": "1F64B-1F3FB",
+ "unicode_alternates": "",
+ "name": "happy person raising one hand tone1",
+ "shortname": ":raising_hand_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "raise",
+ "notice",
+ "attention",
+ "answer"
+ ]
+ },
+ "raising_hand_tone2": {
+ "unicode": "1F64B-1F3FC",
+ "unicode_alternates": "",
+ "name": "happy person raising one hand tone2",
+ "shortname": ":raising_hand_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "raise",
+ "notice",
+ "attention",
+ "answer"
+ ]
+ },
+ "raising_hand_tone3": {
+ "unicode": "1F64B-1F3FD",
+ "unicode_alternates": "",
+ "name": "happy person raising one hand tone3",
+ "shortname": ":raising_hand_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "raise",
+ "notice",
+ "attention",
+ "answer"
+ ]
+ },
+ "raising_hand_tone4": {
+ "unicode": "1F64B-1F3FE",
+ "unicode_alternates": "",
+ "name": "happy person raising one hand tone4",
+ "shortname": ":raising_hand_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "raise",
+ "notice",
+ "attention",
+ "answer"
+ ]
+ },
+ "raising_hand_tone5": {
+ "unicode": "1F64B-1F3FF",
+ "unicode_alternates": "",
+ "name": "happy person raising one hand tone5",
+ "shortname": ":raising_hand_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "woman",
+ "raise",
+ "notice",
+ "attention",
+ "answer"
+ ]
+ },
"ram": {
"unicode": "1F40F",
"unicode_alternates": [],
@@ -10345,7 +22831,16 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "sheep", "ram", "sheep", "male", "horn", "horns"],
+ "keywords": [
+ "animal",
+ "nature",
+ "sheep",
+ "ram",
+ "sheep",
+ "male",
+ "horn",
+ "horns"
+ ],
"moji": "🐏"
},
"ramen": {
@@ -10356,7 +22851,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chipsticks", "food", "japanese", "noodle", "ramen", "noodles", "bowl", "steaming", "soup"],
+ "keywords": [
+ "chipsticks",
+ "food",
+ "japanese",
+ "noodle",
+ "ramen",
+ "noodles",
+ "bowl",
+ "steaming",
+ "soup"
+ ],
"moji": "🍜"
},
"rat": {
@@ -10367,18 +22872,45 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "mouse", "rat", "rodent", "crooked", "snitch"],
+ "keywords": [
+ "animal",
+ "mouse",
+ "rat",
+ "rodent",
+ "crooked",
+ "snitch"
+ ],
"moji": "🐀"
},
+ "record_button": {
+ "unicode": "23FA",
+ "unicode_alternates": "",
+ "name": "black circle for record",
+ "shortname": ":record_button:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sound",
+ "symbol"
+ ]
+ },
"recycle": {
"unicode": "267B",
- "unicode_alternates": ["267B-FE0F"],
+ "unicode_alternates": [
+ "267B-FE0F"
+ ],
"name": "black universal recycling symbol",
"shortname": ":recycle:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "environment", "garbage", "trash"],
+ "keywords": [
+ "arrow",
+ "environment",
+ "garbage",
+ "trash"
+ ],
"moji": "♻"
},
"red_car": {
@@ -10389,7 +22921,10 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle"],
+ "keywords": [
+ "transportation",
+ "vehicle"
+ ],
"moji": "🚗"
},
"red_circle": {
@@ -10400,7 +22935,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "🔴"
},
"registered": {
@@ -10412,17 +22949,28 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alphabet", "circle"]
+ "keywords": [
+ "alphabet",
+ "circle"
+ ]
},
"relaxed": {
"unicode": "263A",
- "unicode_alternates": ["263A-FE0F"],
+ "unicode_alternates": [
+ "263A-FE0F"
+ ],
"name": "white smiling face",
"shortname": ":relaxed:",
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blush", "face", "happiness", "massage", "smile"],
+ "keywords": [
+ "blush",
+ "face",
+ "happiness",
+ "massage",
+ "smile"
+ ],
"moji": "☺"
},
"relieved": {
@@ -10433,7 +22981,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "happiness", "massage", "phew", "relaxed", "relieved", "satisfied", "phew", "relief"],
+ "keywords": [
+ "face",
+ "happiness",
+ "massage",
+ "phew",
+ "relaxed",
+ "relieved",
+ "satisfied",
+ "phew",
+ "relief"
+ ],
"moji": "😌"
},
"reminder_ribbon": {
@@ -10444,7 +23002,9 @@
"category": "celebration",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["awareness"]
+ "keywords": [
+ "awareness"
+ ]
},
"repeat": {
"unicode": "1F501",
@@ -10454,7 +23014,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["loop", "record"],
+ "keywords": [
+ "loop",
+ "record"
+ ],
"moji": "🔁"
},
"repeat_one": {
@@ -10465,7 +23028,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "loop"],
+ "keywords": [
+ "blue-square",
+ "loop"
+ ],
"moji": "🔂"
},
"restroom": {
@@ -10476,7 +23042,17 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "woman", "man", "unisex", "bathroom", "restroom", "sign", "shared", "toilet"],
+ "keywords": [
+ "blue-square",
+ "woman",
+ "man",
+ "unisex",
+ "bathroom",
+ "restroom",
+ "sign",
+ "shared",
+ "toilet"
+ ],
"moji": "🚻"
},
"revolving_hearts": {
@@ -10487,7 +23063,19 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines", "heart", "hearts", "revolving", "moving", "circle", "multiple", "lovers"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines",
+ "heart",
+ "hearts",
+ "revolving",
+ "moving",
+ "circle",
+ "multiple",
+ "lovers"
+ ],
"moji": "💞"
},
"rewind": {
@@ -10498,7 +23086,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "play"],
+ "keywords": [
+ "blue-square",
+ "play"
+ ],
"moji": "⏪"
},
"ribbon": {
@@ -10509,7 +23100,16 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bowtie", "decoration", "girl", "pink", "ribbon", "lace", "wrap", "decorate"],
+ "keywords": [
+ "bowtie",
+ "decoration",
+ "girl",
+ "pink",
+ "ribbon",
+ "lace",
+ "wrap",
+ "decorate"
+ ],
"moji": "🎀"
},
"rice": {
@@ -10520,7 +23120,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "rice", "white", "grain", "food", "bowl"],
+ "keywords": [
+ "food",
+ "rice",
+ "white",
+ "grain",
+ "food",
+ "bowl"
+ ],
"moji": "🍚"
},
"rice_ball": {
@@ -10531,7 +23138,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "japanese", "rice", "ball", "white", "nori", "seaweed", "japanese"],
+ "keywords": [
+ "food",
+ "japanese",
+ "rice",
+ "ball",
+ "white",
+ "nori",
+ "seaweed",
+ "japanese"
+ ],
"moji": "🍙"
},
"rice_cracker": {
@@ -10542,7 +23158,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "japanese", "rice", "cracker", "seaweed", "food", "japanese"],
+ "keywords": [
+ "food",
+ "japanese",
+ "rice",
+ "cracker",
+ "seaweed",
+ "food",
+ "japanese"
+ ],
"moji": "🍘"
},
"rice_scene": {
@@ -10553,7 +23177,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["photo", "moon", "viewing", "observing", "otsukimi", "tsukimi", "rice", "scene", "festival", "autumn"],
+ "keywords": [
+ "photo",
+ "moon",
+ "viewing",
+ "observing",
+ "otsukimi",
+ "tsukimi",
+ "rice",
+ "scene",
+ "festival",
+ "autumn"
+ ],
"moji": "🎑"
},
"right_speaker": {
@@ -10564,7 +23199,13 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sound", "listen", "hear", "noise", "volume"]
+ "keywords": [
+ "sound",
+ "listen",
+ "hear",
+ "noise",
+ "volume"
+ ]
},
"right_speaker_one": {
"unicode": "1F569",
@@ -10572,9 +23213,14 @@
"name": "right speaker with one sound wave",
"shortname": ":right_speaker_one:",
"category": "objects_symbols",
- "aliases": [":right_speaker_with_one_sound_wave:"],
+ "aliases": [
+ ":right_speaker_with_one_sound_wave:"
+ ],
"aliases_ascii": [],
- "keywords": ["low", "volume"]
+ "keywords": [
+ "low",
+ "volume"
+ ]
},
"right_speaker_three": {
"unicode": "1F56A",
@@ -10582,9 +23228,15 @@
"name": "right speaker with three sound waves",
"shortname": ":right_speaker_three:",
"category": "objects_symbols",
- "aliases": [":right_speaker_with_three_sound_waves:"],
+ "aliases": [
+ ":right_speaker_with_three_sound_waves:"
+ ],
"aliases_ascii": [],
- "keywords": ["loud", "high", "volume"]
+ "keywords": [
+ "loud",
+ "high",
+ "volume"
+ ]
},
"ring": {
"unicode": "1F48D",
@@ -10594,7 +23246,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["marriage", "propose", "valentines", "wedding"],
+ "keywords": [
+ "marriage",
+ "propose",
+ "valentines",
+ "wedding"
+ ],
"moji": "💍"
},
"ringing_bell": {
@@ -10605,7 +23262,25 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alert", "ding", "volume", "sound", "chime"]
+ "keywords": [
+ "alert",
+ "ding",
+ "volume",
+ "sound",
+ "chime"
+ ]
+ },
+ "robot": {
+ "unicode": "1F916",
+ "unicode_alternates": "",
+ "name": "robot face",
+ "shortname": ":robot:",
+ "category": "people",
+ "aliases": [
+ ":robot_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
},
"rocket": {
"unicode": "1F680",
@@ -10615,7 +23290,16 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["launch", "ship", "staffmode", "rocket", "space", "spacecraft", "astronaut", "cosmonaut"],
+ "keywords": [
+ "launch",
+ "ship",
+ "staffmode",
+ "rocket",
+ "space",
+ "spacecraft",
+ "astronaut",
+ "cosmonaut"
+ ],
"moji": "🚀"
},
"roller_coaster": {
@@ -10626,9 +23310,34 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["carnival", "fun", "photo", "play", "playground", "roller", "coaster", "amusement", "park", "fair", "ride", "entertainment"],
+ "keywords": [
+ "carnival",
+ "fun",
+ "photo",
+ "play",
+ "playground",
+ "roller",
+ "coaster",
+ "amusement",
+ "park",
+ "fair",
+ "ride",
+ "entertainment"
+ ],
"moji": "🎢"
},
+ "rolling_eyes": {
+ "unicode": "1F644",
+ "unicode_alternates": "",
+ "name": "face with rolling eyes",
+ "shortname": ":rolling_eyes:",
+ "category": "people",
+ "aliases": [
+ ":face_with_rolling_eyes:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"rooster": {
"unicode": "1F413",
"unicode_alternates": [],
@@ -10637,7 +23346,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "chicken", "nature", "rooster", "cockerel", "cock", "male", "cock-a-doodle-doo", "crowing"],
+ "keywords": [
+ "animal",
+ "chicken",
+ "nature",
+ "rooster",
+ "cockerel",
+ "cock",
+ "male",
+ "cock-a-doodle-doo",
+ "crowing"
+ ],
"moji": "🐓"
},
"rose": {
@@ -10648,7 +23367,18 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flowers", "love", "valentines", "rose", "fragrant", "flower", "thorns", "love", "petals", "romance"],
+ "keywords": [
+ "flowers",
+ "love",
+ "valentines",
+ "rose",
+ "fragrant",
+ "flower",
+ "thorns",
+ "love",
+ "petals",
+ "romance"
+ ],
"moji": "🌹"
},
"rosette": {
@@ -10659,7 +23389,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flower"]
+ "keywords": [
+ "flower"
+ ]
},
"rosette_black": {
"unicode": "1F3F6",
@@ -10669,7 +23401,9 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flower"]
+ "keywords": [
+ "flower"
+ ]
},
"rotating_light": {
"unicode": "1F6A8",
@@ -10679,7 +23413,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["911", "ambulance", "emergency", "police", "light", "police", "emergency"],
+ "keywords": [
+ "911",
+ "ambulance",
+ "emergency",
+ "police",
+ "light",
+ "police",
+ "emergency"
+ ],
"moji": "🚨"
},
"round_pushpin": {
@@ -10690,7 +23432,9 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["stationery"],
+ "keywords": [
+ "stationery"
+ ],
"moji": "📍"
},
"rowboat": {
@@ -10701,9 +23445,108 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["hobby", "ship", "sports", "water", "boat", "row", "oar", "paddle"],
+ "keywords": [
+ "hobby",
+ "ship",
+ "sports",
+ "water",
+ "boat",
+ "row",
+ "oar",
+ "paddle"
+ ],
"moji": "🚣"
},
+ "rowboat_tone1": {
+ "unicode": "1F6A3-1F3FB",
+ "unicode_alternates": "",
+ "name": "rowboat tone 1",
+ "shortname": ":rowboat_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hobby",
+ "ship",
+ "water",
+ "boat",
+ "row",
+ "oar",
+ "paddle"
+ ]
+ },
+ "rowboat_tone2": {
+ "unicode": "1F6A3-1F3FC",
+ "unicode_alternates": "",
+ "name": "rowboat tone 2",
+ "shortname": ":rowboat_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hobby",
+ "ship",
+ "water",
+ "boat",
+ "row",
+ "oar",
+ "paddle"
+ ]
+ },
+ "rowboat_tone3": {
+ "unicode": "1F6A3-1F3FD",
+ "unicode_alternates": "",
+ "name": "rowboat tone 3",
+ "shortname": ":rowboat_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hobby",
+ "ship",
+ "water",
+ "boat",
+ "row",
+ "oar",
+ "paddle"
+ ]
+ },
+ "rowboat_tone4": {
+ "unicode": "1F6A3-1F3FE",
+ "unicode_alternates": "",
+ "name": "rowboat tone 4",
+ "shortname": ":rowboat_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hobby",
+ "ship",
+ "water",
+ "boat",
+ "row",
+ "oar",
+ "paddle"
+ ]
+ },
+ "rowboat_tone5": {
+ "unicode": "1F6A3-1F3FF",
+ "unicode_alternates": "",
+ "name": "rowboat tone 5",
+ "shortname": ":rowboat_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "hobby",
+ "ship",
+ "water",
+ "boat",
+ "row",
+ "oar",
+ "paddle"
+ ]
+ },
"rugby_football": {
"unicode": "1F3C9",
"unicode_alternates": [],
@@ -10712,7 +23555,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sports", "rugby", "football", "ball", "sport", "team", "england"],
+ "keywords": [
+ "sports",
+ "rugby",
+ "football",
+ "ball",
+ "sport",
+ "team",
+ "england"
+ ],
"moji": "🏉"
},
"runner": {
@@ -10723,9 +23574,115 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["exercise", "man", "walking", "run", "runner", "jog", "exercise", "sprint", "race", "dash"],
+ "keywords": [
+ "exercise",
+ "man",
+ "walking",
+ "run",
+ "runner",
+ "jog",
+ "exercise",
+ "sprint",
+ "race",
+ "dash"
+ ],
"moji": "🏃"
},
+ "runner_tone1": {
+ "unicode": "1F3C3-1F3FB",
+ "unicode_alternates": "",
+ "name": "runner tone 1",
+ "shortname": ":runner_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "exercise",
+ "man",
+ "run",
+ "jog",
+ "sprint",
+ "race",
+ "dash",
+ "marathon"
+ ]
+ },
+ "runner_tone2": {
+ "unicode": "1F3C3-1F3FC",
+ "unicode_alternates": "",
+ "name": "runner tone 2",
+ "shortname": ":runner_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "exercise",
+ "man",
+ "run",
+ "jog",
+ "sprint",
+ "race",
+ "dash",
+ "marathon"
+ ]
+ },
+ "runner_tone3": {
+ "unicode": "1F3C3-1F3FD",
+ "unicode_alternates": "",
+ "name": "runner tone 3",
+ "shortname": ":runner_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "exercise",
+ "man",
+ "run",
+ "jog",
+ "sprint",
+ "race",
+ "dash",
+ "marathon"
+ ]
+ },
+ "runner_tone4": {
+ "unicode": "1F3C3-1F3FE",
+ "unicode_alternates": "",
+ "name": "runner tone 4",
+ "shortname": ":runner_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "exercise",
+ "man",
+ "run",
+ "jog",
+ "sprint",
+ "race",
+ "dash",
+ "marathon"
+ ]
+ },
+ "runner_tone5": {
+ "unicode": "1F3C3-1F3FF",
+ "unicode_alternates": "",
+ "name": "runner tone 5",
+ "shortname": ":runner_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "exercise",
+ "man",
+ "run",
+ "jog",
+ "sprint",
+ "race",
+ "dash",
+ "marathon"
+ ]
+ },
"running_shirt_with_sash": {
"unicode": "1F3BD",
"unicode_alternates": [],
@@ -10734,29 +23691,73 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["pageant", "play", "running", "run", "shirt", "cloths", "compete", "sports"],
+ "keywords": [
+ "pageant",
+ "play",
+ "running",
+ "run",
+ "shirt",
+ "cloths",
+ "compete",
+ "sports"
+ ],
"moji": "🎽"
},
+ "sa": {
+ "unicode": "1F202",
+ "unicode_alternates": "",
+ "name": "squared katakana sa",
+ "shortname": ":sa:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "blue-square",
+ "japanese",
+ "symbol",
+ "word"
+ ]
+ },
"sagittarius": {
"unicode": "2650",
- "unicode_alternates": ["2650-FE0F"],
+ "unicode_alternates": [
+ "2650-FE0F"
+ ],
"name": "sagittarius",
"shortname": ":sagittarius:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sagittarius", "centaur", "archer", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "sagittarius",
+ "centaur",
+ "archer",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♐"
},
"sailboat": {
"unicode": "26F5",
- "unicode_alternates": ["26F5-FE0F"],
+ "unicode_alternates": [
+ "26F5-FE0F"
+ ],
"name": "sailboat",
"shortname": ":sailboat:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["ship", "transportation"],
+ "keywords": [
+ "ship",
+ "transportation"
+ ],
"moji": "⛵"
},
"sake": {
@@ -10767,7 +23768,19 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["beverage", "drink", "drunk", "wine", "sake", "wine", "rice", "ferment", "alcohol", "japanese", "drink"],
+ "keywords": [
+ "beverage",
+ "drink",
+ "drunk",
+ "wine",
+ "sake",
+ "wine",
+ "rice",
+ "ferment",
+ "alcohol",
+ "japanese",
+ "drink"
+ ],
"moji": "🍶"
},
"sandal": {
@@ -10778,7 +23791,10 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fashion", "shoes"],
+ "keywords": [
+ "fashion",
+ "shoes"
+ ],
"moji": "👡"
},
"santa": {
@@ -10789,9 +23805,159 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["christmas", "father christmas", "festival", "male", "man", "xmas", "santa", "saint nick", "jolly", "ho ho ho", "north pole", "presents", "gifts", "naughty", "nice", "sleigh", "father", "christmas", "holiday"],
+ "keywords": [
+ "christmas",
+ "father christmas",
+ "festival",
+ "male",
+ "man",
+ "xmas",
+ "santa",
+ "saint nick",
+ "jolly",
+ "ho ho ho",
+ "north pole",
+ "presents",
+ "gifts",
+ "naughty",
+ "nice",
+ "sleigh",
+ "father",
+ "christmas",
+ "holiday"
+ ],
"moji": "🎅"
},
+ "santa_tone1": {
+ "unicode": "1F385-1F3FB",
+ "unicode_alternates": "",
+ "name": "father christmas tone 1",
+ "shortname": ":santa_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "festival",
+ "male",
+ "man",
+ "xmas",
+ "santa",
+ "saint nick",
+ "jolly",
+ "ho ho ho",
+ "north pole",
+ "presents",
+ "gifts",
+ "naughty",
+ "nice",
+ "sleigh",
+ "holiday"
+ ]
+ },
+ "santa_tone2": {
+ "unicode": "1F385-1F3FC",
+ "unicode_alternates": "",
+ "name": "father christmas tone 2",
+ "shortname": ":santa_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "festival",
+ "male",
+ "man",
+ "xmas",
+ "santa",
+ "saint nick",
+ "jolly",
+ "ho ho ho",
+ "north pole",
+ "presents",
+ "gifts",
+ "naughty",
+ "nice",
+ "sleigh",
+ "holiday"
+ ]
+ },
+ "santa_tone3": {
+ "unicode": "1F385-1F3FD",
+ "unicode_alternates": "",
+ "name": "father christmas tone 3",
+ "shortname": ":santa_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "festival",
+ "male",
+ "man",
+ "xmas",
+ "santa",
+ "saint nick",
+ "jolly",
+ "ho ho ho",
+ "north pole",
+ "presents",
+ "gifts",
+ "naughty",
+ "nice",
+ "sleigh",
+ "holiday"
+ ]
+ },
+ "santa_tone4": {
+ "unicode": "1F385-1F3FE",
+ "unicode_alternates": "",
+ "name": "father christmas tone 4",
+ "shortname": ":santa_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "festival",
+ "male",
+ "man",
+ "xmas",
+ "santa",
+ "saint nick",
+ "jolly",
+ "ho ho ho",
+ "north pole",
+ "presents",
+ "gifts",
+ "naughty",
+ "nice",
+ "sleigh",
+ "holiday"
+ ]
+ },
+ "santa_tone5": {
+ "unicode": "1F385-1F3FF",
+ "unicode_alternates": "",
+ "name": "father christmas tone 5",
+ "shortname": ":santa_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "festival",
+ "male",
+ "man",
+ "xmas",
+ "santa",
+ "saint nick",
+ "jolly",
+ "ho ho ho",
+ "north pole",
+ "presents",
+ "gifts",
+ "naughty",
+ "nice",
+ "sleigh",
+ "holiday"
+ ]
+ },
"satellite": {
"unicode": "1F4E1",
"unicode_alternates": [],
@@ -10800,7 +23966,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication"],
+ "keywords": [
+ "communication"
+ ],
"moji": "📡"
},
"satellite_orbital": {
@@ -10811,7 +23979,11 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "orbital", "space"]
+ "keywords": [
+ "communication",
+ "orbital",
+ "space"
+ ]
},
"saxophone": {
"unicode": "1F3B7",
@@ -10821,9 +23993,35 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["instrument", "music", "saxophone", "sax", "music", "instrument", "woodwind"],
+ "keywords": [
+ "instrument",
+ "music",
+ "saxophone",
+ "sax",
+ "music",
+ "instrument",
+ "woodwind"
+ ],
"moji": "🎷"
},
+ "scales": {
+ "unicode": "2696",
+ "unicode_alternates": "",
+ "name": "scales",
+ "shortname": ":scales:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "balance",
+ "justice",
+ "libra",
+ "object",
+ "tool",
+ "weight",
+ "zodiac"
+ ]
+ },
"school": {
"unicode": "1F3EB",
"unicode_alternates": [],
@@ -10832,7 +24030,17 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["building", "school", "university", "elementary", "middle", "high", "college", "teach", "education"],
+ "keywords": [
+ "building",
+ "school",
+ "university",
+ "elementary",
+ "middle",
+ "high",
+ "college",
+ "teach",
+ "education"
+ ],
"moji": "🏫"
},
"school_satchel": {
@@ -10843,29 +24051,74 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bag", "education", "student", "school", "satchel", "backpack", "bag", "packing", "pack", "hike", "education", "adventure", "travel", "sightsee"],
+ "keywords": [
+ "bag",
+ "education",
+ "student",
+ "school",
+ "satchel",
+ "backpack",
+ "bag",
+ "packing",
+ "pack",
+ "hike",
+ "education",
+ "adventure",
+ "travel",
+ "sightsee"
+ ],
"moji": "🎒"
},
"scissors": {
"unicode": "2702",
- "unicode_alternates": ["2702-FE0F"],
+ "unicode_alternates": [
+ "2702-FE0F"
+ ],
"name": "black scissors",
"shortname": ":scissors:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cut", "stationery"],
+ "keywords": [
+ "cut",
+ "stationery"
+ ],
"moji": "✂"
},
+ "scorpion": {
+ "unicode": "1F982",
+ "unicode_alternates": "",
+ "name": "scorpion",
+ "shortname": ":scorpion:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"scorpius": {
"unicode": "264F",
- "unicode_alternates": ["264F-FE0F"],
+ "unicode_alternates": [
+ "264F-FE0F"
+ ],
"name": "scorpius",
"shortname": ":scorpius:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["scorpius", "scorpion", "scorpio", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "scorpius",
+ "scorpion",
+ "scorpio",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♏"
},
"scream": {
@@ -10876,7 +24129,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "munch", "scream", "painting", "artist", "alien"],
+ "keywords": [
+ "face",
+ "munch",
+ "scream",
+ "painting",
+ "artist",
+ "alien"
+ ],
"moji": "😱"
},
"scream_cat": {
@@ -10887,7 +24147,22 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cats", "munch", "weary", "sleepy", "tired", "tiredness", "study", "finals", "school", "exhausted", "scream", "painting", "artist"],
+ "keywords": [
+ "animal",
+ "cats",
+ "munch",
+ "weary",
+ "sleepy",
+ "tired",
+ "tiredness",
+ "study",
+ "finals",
+ "school",
+ "exhausted",
+ "scream",
+ "painting",
+ "artist"
+ ],
"moji": "🙀"
},
"scroll": {
@@ -10898,7 +24173,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["documents"],
+ "keywords": [
+ "documents"
+ ],
"moji": "📜"
},
"seat": {
@@ -10909,18 +24186,24 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sit"],
+ "keywords": [
+ "sit"
+ ],
"moji": "💺"
},
"secret": {
"unicode": "3299",
- "unicode_alternates": ["3299-FE0F"],
+ "unicode_alternates": [
+ "3299-FE0F"
+ ],
"name": "circled ideograph secret",
"shortname": ":secret:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["privacy"],
+ "keywords": [
+ "privacy"
+ ],
"moji": "㊙"
},
"see_no_evil": {
@@ -10931,7 +24214,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "monkey", "nature", "monkey", "see", "eyes", "vision", "sight", "mizaru"],
+ "keywords": [
+ "animal",
+ "monkey",
+ "nature",
+ "monkey",
+ "see",
+ "eyes",
+ "vision",
+ "sight",
+ "mizaru"
+ ],
"moji": "🙈"
},
"seedling": {
@@ -10942,19 +24235,49 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["grass", "lawn", "nature", "plant", "seedling", "plant", "new", "start", "grow"],
+ "keywords": [
+ "grass",
+ "lawn",
+ "nature",
+ "plant",
+ "seedling",
+ "plant",
+ "new",
+ "start",
+ "grow"
+ ],
"moji": "🌱"
},
"seven": {
"moji": "7️⃣",
"unicode": "0037-20E3",
- "unicode_alternates": ["0037-FE0F-20E3"],
+ "unicode_alternates": [
+ "0037-FE0F-20E3"
+ ],
"name": "digit seven",
"shortname": ":seven:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["7", "blue-square", "numbers", "prime"]
+ "keywords": [
+ "7",
+ "blue-square",
+ "numbers",
+ "prime"
+ ]
+ },
+ "shamrock": {
+ "unicode": "2618",
+ "unicode_alternates": "",
+ "name": "shamrock",
+ "shortname": ":shamrock:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "nature",
+ "plant"
+ ]
},
"shaved_ice": {
"unicode": "1F367",
@@ -10964,7 +24287,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["desert", "hot", "shaved", "ice", "dessert", "treat", "syrup", "flavoring"],
+ "keywords": [
+ "desert",
+ "hot",
+ "shaved",
+ "ice",
+ "dessert",
+ "treat",
+ "syrup",
+ "flavoring"
+ ],
"moji": "🍧"
},
"sheep": {
@@ -10975,7 +24307,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "sheep", "wool", "flock", "follower", "ewe", "female", "lamb"],
+ "keywords": [
+ "animal",
+ "nature",
+ "sheep",
+ "wool",
+ "flock",
+ "follower",
+ "ewe",
+ "female",
+ "lamb"
+ ],
"moji": "🐑"
},
"shell": {
@@ -10986,7 +24328,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["beach", "nature", "sea", "shell", "spiral", "beach", "sand", "crab", "nautilus"],
+ "keywords": [
+ "beach",
+ "nature",
+ "sea",
+ "shell",
+ "spiral",
+ "beach",
+ "sand",
+ "crab",
+ "nautilus"
+ ],
"moji": "🐚"
},
"shield": {
@@ -10997,7 +24349,26 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["interstate", "route", "sign", "highway", "interstate"]
+ "keywords": [
+ "interstate",
+ "route",
+ "sign",
+ "highway",
+ "interstate"
+ ]
+ },
+ "shinto_shrine": {
+ "unicode": "26E9",
+ "unicode_alternates": "",
+ "name": "shinto shrine",
+ "shortname": ":shinto_shrine:",
+ "category": "travel",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "religion",
+ "symbol"
+ ]
},
"ship": {
"unicode": "1F6A2",
@@ -11007,7 +24378,13 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["titanic", "transportation", "ferry", "ship", "boat"],
+ "keywords": [
+ "titanic",
+ "transportation",
+ "ferry",
+ "ship",
+ "boat"
+ ],
"moji": "🚢"
},
"shirt": {
@@ -11018,7 +24395,10 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cloth", "fashion"],
+ "keywords": [
+ "cloth",
+ "fashion"
+ ],
"moji": "👕"
},
"shopping_bags": {
@@ -11029,7 +24409,13 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["purchase", "mall", "buy", "store", "shop"]
+ "keywords": [
+ "purchase",
+ "mall",
+ "buy",
+ "store",
+ "shop"
+ ]
},
"shower": {
"unicode": "1F6BF",
@@ -11039,7 +24425,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bath", "clean", "wash", "bathroom", "shower", "soap", "water", "clean", "shampoo", "lather"],
+ "keywords": [
+ "bath",
+ "clean",
+ "wash",
+ "bathroom",
+ "shower",
+ "soap",
+ "water",
+ "clean",
+ "shampoo",
+ "lather"
+ ],
"moji": "🚿"
},
"signal_strength": {
@@ -11050,19 +24447,27 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "📶"
},
"six": {
"moji": "6️⃣",
"unicode": "0036-20E3",
- "unicode_alternates": ["0036-FE0F-20E3"],
+ "unicode_alternates": [
+ "0036-FE0F-20E3"
+ ],
"name": "digit six",
"shortname": ":six:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["6", "blue-square", "numbers"]
+ "keywords": [
+ "6",
+ "blue-square",
+ "numbers"
+ ]
},
"six_pointed_star": {
"unicode": "1F52F",
@@ -11072,7 +24477,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["purple-square"],
+ "keywords": [
+ "purple-square"
+ ],
"moji": "🔯"
},
"ski": {
@@ -11083,20 +24490,75 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cold", "sports", "winter", "ski", "downhill", "cross-country", "poles", "snow", "winter", "mountain", "alpine", "powder", "slalom", "freestyle"],
+ "keywords": [
+ "cold",
+ "sports",
+ "winter",
+ "ski",
+ "downhill",
+ "cross-country",
+ "poles",
+ "snow",
+ "winter",
+ "mountain",
+ "alpine",
+ "powder",
+ "slalom",
+ "freestyle"
+ ],
"moji": "🎿"
},
+ "skier": {
+ "unicode": "26F7",
+ "unicode_alternates": "",
+ "name": "skier",
+ "shortname": ":skier:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "person",
+ "ski",
+ "snow",
+ "sport",
+ "travel"
+ ]
+ },
"skull": {
"unicode": "1F480",
"unicode_alternates": [],
"name": "skull",
"shortname": ":skull:",
"category": "emoticons",
- "aliases": [":skeleton:"],
- "aliases_ascii": [],
- "keywords": ["dead", "skeleton", "dying"],
+ "aliases": [
+ ":skeleton:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "dead",
+ "skeleton",
+ "dying"
+ ],
"moji": "💀"
},
+ "skull_crossbones": {
+ "unicode": "2620",
+ "unicode_alternates": "",
+ "name": "skull and crossbones",
+ "shortname": ":skull_crossbones:",
+ "category": "objects",
+ "aliases": [
+ ":skull_and_crossbones:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "body",
+ "death",
+ "face",
+ "monster",
+ "person"
+ ]
+ },
"sleeping": {
"unicode": "1F634",
"unicode_alternates": [],
@@ -11105,7 +24567,15 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "sleepy", "tired", "sleep", "sleepy", "sleeping", "snore"],
+ "keywords": [
+ "face",
+ "sleepy",
+ "tired",
+ "sleep",
+ "sleepy",
+ "sleeping",
+ "snore"
+ ],
"moji": "😴"
},
"sleeping_accommodation": {
@@ -11116,7 +24586,11 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["hotel", "motel", "rest"]
+ "keywords": [
+ "hotel",
+ "motel",
+ "rest"
+ ]
},
"sleepy": {
"unicode": "1F62A",
@@ -11126,7 +24600,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "rest", "tired", "sleepy", "tired", "exhausted"],
+ "keywords": [
+ "face",
+ "rest",
+ "tired",
+ "sleepy",
+ "tired",
+ "exhausted"
+ ],
"moji": "😪"
},
"slight_frown": {
@@ -11135,9 +24616,16 @@
"name": "slightly frowning face",
"shortname": ":slight_frown:",
"category": "people",
- "aliases": [":slightly_frowning_face:"],
+ "aliases": [
+ ":slightly_frowning_face:"
+ ],
"aliases_ascii": [],
- "keywords": ["slight", "frown", "unhappy", "disappointed"]
+ "keywords": [
+ "slight",
+ "frown",
+ "unhappy",
+ "disappointed"
+ ]
},
"slight_smile": {
"unicode": "1F642",
@@ -11145,9 +24633,15 @@
"name": "slightly smiling face",
"shortname": ":slight_smile:",
"category": "people",
- "aliases": [":slightly_smiling_face:"],
+ "aliases": [
+ ":slightly_smiling_face:"
+ ],
"aliases_ascii": [],
- "keywords": ["slight", "smile", "happy"]
+ "keywords": [
+ "slight",
+ "smile",
+ "happy"
+ ]
},
"slot_machine": {
"unicode": "1F3B0",
@@ -11157,7 +24651,17 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bet", "gamble", "vegas", "slot", "machine", "gamble", "one-armed bandit", "slots", "luck"],
+ "keywords": [
+ "bet",
+ "gamble",
+ "vegas",
+ "slot",
+ "machine",
+ "gamble",
+ "one-armed bandit",
+ "slots",
+ "luck"
+ ],
"moji": "🎰"
},
"small_blue_diamond": {
@@ -11168,7 +24672,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "🔹"
},
"small_orange_diamond": {
@@ -11179,7 +24685,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "🔸"
},
"small_red_triangle": {
@@ -11190,7 +24698,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "🔺"
},
"small_red_triangle_down": {
@@ -11201,7 +24711,9 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "🔻"
},
"smile": {
@@ -11211,8 +24723,24 @@
"shortname": ":smile:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":)", ":-)", "=]", "=)", ":]"],
- "keywords": ["face", "funny", "haha", "happy", "joy", "laugh", "smile", "smiley", "smiling"],
+ "aliases_ascii": [
+ ":)",
+ ":-)",
+ "=]",
+ "=)",
+ ":]"
+ ],
+ "keywords": [
+ "face",
+ "funny",
+ "haha",
+ "happy",
+ "joy",
+ "laugh",
+ "smile",
+ "smiley",
+ "smiling"
+ ],
"moji": "😄"
},
"smile_cat": {
@@ -11223,7 +24751,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cats", "cat", "smile", "grin", "grinning"],
+ "keywords": [
+ "animal",
+ "cats",
+ "cat",
+ "smile",
+ "grin",
+ "grinning"
+ ],
"moji": "😸"
},
"smiley": {
@@ -11233,8 +24768,20 @@
"shortname": ":smiley:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":D", ":-D", "=D"],
- "keywords": ["face", "haha", "happy", "joy", "smiling", "smile", "smiley"],
+ "aliases_ascii": [
+ ":D",
+ ":-D",
+ "=D"
+ ],
+ "keywords": [
+ "face",
+ "haha",
+ "happy",
+ "joy",
+ "smiling",
+ "smile",
+ "smiley"
+ ],
"moji": "😃"
},
"smiley_cat": {
@@ -11245,7 +24792,15 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cats", "happy", "smile", "smiley", "cat", "happy"],
+ "keywords": [
+ "animal",
+ "cats",
+ "happy",
+ "smile",
+ "smiley",
+ "cat",
+ "happy"
+ ],
"moji": "😺"
},
"smiling_imp": {
@@ -11256,7 +24811,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["devil", "horns", "horns", "devil", "impish", "trouble"],
+ "keywords": [
+ "devil",
+ "horns",
+ "horns",
+ "devil",
+ "impish",
+ "trouble"
+ ],
"moji": "😈"
},
"smirk": {
@@ -11267,7 +24829,18 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mean", "prank", "smile", "smug", "smirking", "smirk", "smug", "smile", "half-smile", "conceited"],
+ "keywords": [
+ "mean",
+ "prank",
+ "smile",
+ "smug",
+ "smirking",
+ "smirk",
+ "smug",
+ "smile",
+ "half-smile",
+ "conceited"
+ ],
"moji": "😏"
},
"smirk_cat": {
@@ -11278,7 +24851,15 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cats", "smirk", "smirking", "wry", "confident", "confidence"],
+ "keywords": [
+ "animal",
+ "cats",
+ "smirk",
+ "smirking",
+ "wry",
+ "confident",
+ "confidence"
+ ],
"moji": "😼"
},
"smoking": {
@@ -11289,7 +24870,19 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cigarette", "kills", "tobacco", "smoking", "cigarette", "smoke", "cancer", "lungs", "inhale", "tar", "nicotine"],
+ "keywords": [
+ "cigarette",
+ "kills",
+ "tobacco",
+ "smoking",
+ "cigarette",
+ "smoke",
+ "cancer",
+ "lungs",
+ "inhale",
+ "tar",
+ "nicotine"
+ ],
"moji": "🚬"
},
"snail": {
@@ -11300,7 +24893,16 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "shell", "slow", "snail", "slow", "escargot", "french", "appetizer"],
+ "keywords": [
+ "animal",
+ "shell",
+ "slow",
+ "snail",
+ "slow",
+ "escargot",
+ "french",
+ "appetizer"
+ ],
"moji": "🐌"
},
"snake": {
@@ -11311,7 +24913,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "evil"],
+ "keywords": [
+ "animal",
+ "evil"
+ ],
"moji": "🐍"
},
"snowboarder": {
@@ -11322,31 +24927,89 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sports", "winter", "snow", "boarding", "sports", "freestyle", "halfpipe", "board", "mountain", "alpine", "winter"],
+ "keywords": [
+ "sports",
+ "winter",
+ "snow",
+ "boarding",
+ "sports",
+ "freestyle",
+ "halfpipe",
+ "board",
+ "mountain",
+ "alpine",
+ "winter"
+ ],
"moji": "🏂"
},
"snowflake": {
"unicode": "2744",
- "unicode_alternates": ["2744-FE0F"],
+ "unicode_alternates": [
+ "2744-FE0F"
+ ],
"name": "snowflake",
"shortname": ":snowflake:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["christmas", "cold", "season", "weather", "winter", "xmas", "snowflake", "snow", "frozen", "droplet", "ice", "crystal", "cold", "chilly", "winter", "unique", "special", "below zero", "elsa"],
+ "keywords": [
+ "christmas",
+ "cold",
+ "season",
+ "weather",
+ "winter",
+ "xmas",
+ "snowflake",
+ "snow",
+ "frozen",
+ "droplet",
+ "ice",
+ "crystal",
+ "cold",
+ "chilly",
+ "winter",
+ "unique",
+ "special",
+ "below zero",
+ "elsa"
+ ],
"moji": "❄"
},
"snowman": {
"unicode": "26C4",
- "unicode_alternates": ["26C4-FE0F"],
+ "unicode_alternates": [
+ "26C4-FE0F"
+ ],
"name": "snowman without snow",
"shortname": ":snowman:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["christmas", "cold", "season", "weather", "winter", "xmas"],
+ "keywords": [
+ "christmas",
+ "cold",
+ "season",
+ "weather",
+ "winter",
+ "xmas"
+ ],
"moji": "⛄"
},
+ "snowman2": {
+ "unicode": "2603",
+ "unicode_alternates": "",
+ "name": "snowman",
+ "shortname": ":snowman2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "cold",
+ "nature",
+ "snow",
+ "weather"
+ ]
+ },
"sob": {
"unicode": "1F62D",
"unicode_alternates": [],
@@ -11355,18 +25018,41 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cry", "face", "sad", "tears", "upset", "cry", "sob", "tears", "sad", "melancholy", "morn", "somber", "hurt"],
+ "keywords": [
+ "cry",
+ "face",
+ "sad",
+ "tears",
+ "upset",
+ "cry",
+ "sob",
+ "tears",
+ "sad",
+ "melancholy",
+ "morn",
+ "somber",
+ "hurt"
+ ],
"moji": "😭"
},
"soccer": {
"unicode": "26BD",
- "unicode_alternates": ["26BD-FE0F"],
+ "unicode_alternates": [
+ "26BD-FE0F"
+ ],
"name": "soccer ball",
"shortname": ":soccer:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["balls", "fifa", "football", "sports", "european", "football"],
+ "keywords": [
+ "balls",
+ "fifa",
+ "football",
+ "sports",
+ "european",
+ "football"
+ ],
"moji": "⚽"
},
"soon": {
@@ -11377,7 +25063,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arrow", "words"],
+ "keywords": [
+ "arrow",
+ "words"
+ ],
"moji": "🔜"
},
"sos": {
@@ -11388,7 +25077,12 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["emergency", "help", "red-square", "words"],
+ "keywords": [
+ "emergency",
+ "help",
+ "red-square",
+ "words"
+ ],
"moji": "🆘"
},
"sound": {
@@ -11399,7 +25093,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["speaker", "volume"],
+ "keywords": [
+ "speaker",
+ "volume"
+ ],
"moji": "🔉"
},
"space_invader": {
@@ -11410,18 +25107,26 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arcade", "game"],
+ "keywords": [
+ "arcade",
+ "game"
+ ],
"moji": "👾"
},
"spades": {
"unicode": "2660",
- "unicode_alternates": ["2660-FE0F"],
+ "unicode_alternates": [
+ "2660-FE0F"
+ ],
"name": "black spade suit",
"shortname": ":spades:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cards", "poker"],
+ "keywords": [
+ "cards",
+ "poker"
+ ],
"moji": "♠"
},
"spaghetti": {
@@ -11432,18 +25137,32 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "italian", "noodle", "spaghetti", "noodles", "tomato", "sauce", "italian"],
+ "keywords": [
+ "food",
+ "italian",
+ "noodle",
+ "spaghetti",
+ "noodles",
+ "tomato",
+ "sauce",
+ "italian"
+ ],
"moji": "🍝"
},
"sparkle": {
"unicode": "2747",
- "unicode_alternates": ["2747-FE0F"],
+ "unicode_alternates": [
+ "2747-FE0F"
+ ],
"name": "sparkle",
"shortname": ":sparkle:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["green-square", "stars"],
+ "keywords": [
+ "green-square",
+ "stars"
+ ],
"moji": "❇"
},
"sparkler": {
@@ -11454,7 +25173,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["night", "shine", "stars"],
+ "keywords": [
+ "night",
+ "shine",
+ "stars"
+ ],
"moji": "🎇"
},
"sparkles": {
@@ -11465,7 +25188,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cool", "shine", "shiny", "stars"],
+ "keywords": [
+ "cool",
+ "shine",
+ "shiny",
+ "stars"
+ ],
"moji": "✨"
},
"sparkling_heart": {
@@ -11476,7 +25204,12 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines"
+ ],
"moji": "💖"
},
"speak_no_evil": {
@@ -11487,7 +25220,19 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "monkey", "monkey", "mouth", "talk", "say", "words", "verbal", "verbalize", "oral", "iwazaru"],
+ "keywords": [
+ "animal",
+ "monkey",
+ "monkey",
+ "mouth",
+ "talk",
+ "say",
+ "words",
+ "verbal",
+ "verbalize",
+ "oral",
+ "iwazaru"
+ ],
"moji": "🙊"
},
"speaker": {
@@ -11498,7 +25243,12 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sound", "listen", "hear", "noise"]
+ "keywords": [
+ "sound",
+ "listen",
+ "hear",
+ "noise"
+ ]
},
"speaking_head": {
"unicode": "1F5E3",
@@ -11506,9 +25256,13 @@
"name": "speaking head in silhouette",
"shortname": ":speaking_head:",
"category": "objects_symbols",
- "aliases": [":speaking_head_in_silhouette:"],
+ "aliases": [
+ ":speaking_head_in_silhouette:"
+ ],
"aliases_ascii": [],
- "keywords": ["talk"]
+ "keywords": [
+ "talk"
+ ]
},
"speech_balloon": {
"unicode": "1F4AC",
@@ -11518,7 +25272,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bubble", "words", "speech", "balloon", "talk", "conversation", "communication", "comic", "dialogue"],
+ "keywords": [
+ "bubble",
+ "words",
+ "speech",
+ "balloon",
+ "talk",
+ "conversation",
+ "communication",
+ "comic",
+ "dialogue"
+ ],
"moji": "💬"
},
"speech_left": {
@@ -11527,9 +25291,19 @@
"name": "left speech bubble",
"shortname": ":speech_left:",
"category": "objects_symbols",
- "aliases": [":left_speech_bubble:"],
- "aliases_ascii": [],
- "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"]
+ "aliases": [
+ ":left_speech_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "balloon",
+ "words",
+ "talk",
+ "conversation",
+ "communication",
+ "comic",
+ "dialogue"
+ ]
},
"speech_right": {
"unicode": "1F5E9",
@@ -11537,9 +25311,19 @@
"name": "right speech bubble",
"shortname": ":speech_right:",
"category": "objects_symbols",
- "aliases": [":right_speech_bubble:"],
- "aliases_ascii": [],
- "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"]
+ "aliases": [
+ ":right_speech_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "balloon",
+ "words",
+ "talk",
+ "conversation",
+ "communication",
+ "comic",
+ "dialogue"
+ ]
},
"speech_three": {
"unicode": "1F5EB",
@@ -11547,9 +25331,19 @@
"name": "three speech bubbles",
"shortname": ":speech_three:",
"category": "objects_symbols",
- "aliases": [":three_speech_bubbles:"],
- "aliases_ascii": [],
- "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"]
+ "aliases": [
+ ":three_speech_bubbles:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "balloon",
+ "words",
+ "talk",
+ "conversation",
+ "communication",
+ "comic",
+ "dialogue"
+ ]
},
"speech_two": {
"unicode": "1F5EA",
@@ -11557,9 +25351,19 @@
"name": "two speech bubbles",
"shortname": ":speech_two:",
"category": "objects_symbols",
- "aliases": [":two_speech_bubbles:"],
- "aliases_ascii": [],
- "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"]
+ "aliases": [
+ ":two_speech_bubbles:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "balloon",
+ "words",
+ "talk",
+ "conversation",
+ "communication",
+ "comic",
+ "dialogue"
+ ]
},
"speedboat": {
"unicode": "1F6A4",
@@ -11569,7 +25373,16 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["ship", "transportation", "vehicle", "motor", "speed", "ski", "power", "boat"],
+ "keywords": [
+ "ship",
+ "transportation",
+ "vehicle",
+ "motor",
+ "speed",
+ "ski",
+ "power",
+ "boat"
+ ],
"moji": "🚤"
},
"spider": {
@@ -11580,7 +25393,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["arachnid", "eight-legged"]
+ "keywords": [
+ "arachnid",
+ "eight-legged"
+ ]
},
"spider_web": {
"unicode": "1F578",
@@ -11590,7 +25406,9 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cobweb"]
+ "keywords": [
+ "cobweb"
+ ]
},
"spy": {
"unicode": "1F575",
@@ -11598,9 +25416,100 @@
"name": "sleuth or spy",
"shortname": ":spy:",
"category": "people",
- "aliases": [":sleuth_or_spy:"],
+ "aliases": [
+ ":sleuth_or_spy:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "pi",
+ "undercover",
+ "investigator"
+ ]
+ },
+ "spy_tone1": {
+ "unicode": "1F575-1F3FB",
+ "unicode_alternates": "",
+ "name": "sleuth or spy tone 1",
+ "shortname": ":spy_tone1:",
+ "category": "people",
+ "aliases": [
+ ":sleuth_or_spy_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "pi",
+ "undercover",
+ "investigator",
+ "person"
+ ]
+ },
+ "spy_tone2": {
+ "unicode": "1F575-1F3FC",
+ "unicode_alternates": "",
+ "name": "sleuth or spy tone 2",
+ "shortname": ":spy_tone2:",
+ "category": "people",
+ "aliases": [
+ ":sleuth_or_spy_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "pi",
+ "undercover",
+ "investigator",
+ "person"
+ ]
+ },
+ "spy_tone3": {
+ "unicode": "1F575-1F3FD",
+ "unicode_alternates": "",
+ "name": "sleuth or spy tone 3",
+ "shortname": ":spy_tone3:",
+ "category": "people",
+ "aliases": [
+ ":sleuth_or_spy_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "pi",
+ "undercover",
+ "investigator",
+ "person"
+ ]
+ },
+ "spy_tone4": {
+ "unicode": "1F575-1F3FE",
+ "unicode_alternates": "",
+ "name": "sleuth or spy tone 4",
+ "shortname": ":spy_tone4:",
+ "category": "people",
+ "aliases": [
+ ":sleuth_or_spy_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "pi",
+ "undercover",
+ "investigator",
+ "person"
+ ]
+ },
+ "spy_tone5": {
+ "unicode": "1F575-1F3FF",
+ "unicode_alternates": "",
+ "name": "sleuth or spy tone 5",
+ "shortname": ":spy_tone5:",
+ "category": "people",
+ "aliases": [
+ ":sleuth_or_spy_tone5:"
+ ],
"aliases_ascii": [],
- "keywords": ["pi", "undercover", "investigator"]
+ "keywords": [
+ "pi",
+ "undercover",
+ "investigator",
+ "person"
+ ]
},
"stadium": {
"unicode": "1F3DF",
@@ -11610,17 +25519,28 @@
"category": "travel_places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sport", "event", "concert", "convention", "game"]
+ "keywords": [
+ "sport",
+ "event",
+ "concert",
+ "convention",
+ "game"
+ ]
},
"star": {
"unicode": "2B50",
- "unicode_alternates": ["2B50-FE0F"],
+ "unicode_alternates": [
+ "2B50-FE0F"
+ ],
"name": "white medium star",
"shortname": ":star:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["night", "yellow"],
+ "keywords": [
+ "night",
+ "yellow"
+ ],
"moji": "⭐"
},
"star2": {
@@ -11631,9 +25551,48 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["night", "sparkle", "glow", "glowing", "star", "five", "points", "classic"],
+ "keywords": [
+ "night",
+ "sparkle",
+ "glow",
+ "glowing",
+ "star",
+ "five",
+ "points",
+ "classic"
+ ],
"moji": "🌟"
},
+ "star_and_crescent": {
+ "unicode": "262A",
+ "unicode_alternates": "",
+ "name": "star and crescent",
+ "shortname": ":star_and_crescent:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "islam",
+ "muslim",
+ "religion",
+ "symbol"
+ ]
+ },
+ "star_of_david": {
+ "unicode": "2721",
+ "unicode_alternates": "",
+ "name": "star of david",
+ "shortname": ":star_of_david:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "jew",
+ "jewish",
+ "religion",
+ "symbol"
+ ]
+ },
"stars": {
"unicode": "1F320",
"unicode_alternates": [],
@@ -11642,7 +25601,17 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["night", "photo", "shooting", "shoot", "star", "sky", "night", "comet", "meteoroid"],
+ "keywords": [
+ "night",
+ "photo",
+ "shooting",
+ "shoot",
+ "star",
+ "sky",
+ "night",
+ "comet",
+ "meteoroid"
+ ],
"moji": "🌠"
},
"station": {
@@ -11653,7 +25622,14 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["public", "transportation", "vehicle", "station", "train", "subway"],
+ "keywords": [
+ "public",
+ "transportation",
+ "vehicle",
+ "station",
+ "train",
+ "subway"
+ ],
"moji": "🚉"
},
"statue_of_liberty": {
@@ -11664,7 +25640,10 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["american", "newyork"],
+ "keywords": [
+ "american",
+ "newyork"
+ ],
"moji": "🗽"
},
"steam_locomotive": {
@@ -11675,7 +25654,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["train", "transportation", "vehicle", "locomotive", "steam", "train", "engine"],
+ "keywords": [
+ "train",
+ "transportation",
+ "vehicle",
+ "locomotive",
+ "steam",
+ "train",
+ "engine"
+ ],
"moji": "🚂"
},
"stereo": {
@@ -11684,9 +25671,17 @@
"name": "portable stereo",
"shortname": ":stereo:",
"category": "objects_symbols",
- "aliases": [":portable_stereo:"],
- "aliases_ascii": [],
- "keywords": ["communication", "music", "program", "boom", "box"]
+ "aliases": [
+ ":portable_stereo:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "communication",
+ "music",
+ "program",
+ "boom",
+ "box"
+ ]
},
"stew": {
"unicode": "1F372",
@@ -11696,7 +25691,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "meat", "stew", "hearty", "soup", "thick", "hot", "pot"],
+ "keywords": [
+ "food",
+ "meat",
+ "stew",
+ "hearty",
+ "soup",
+ "thick",
+ "hot",
+ "pot"
+ ],
"moji": "🍲"
},
"stock_chart": {
@@ -11707,7 +25711,39 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["graph", "presentation", "stats", "business"]
+ "keywords": [
+ "graph",
+ "presentation",
+ "stats",
+ "business"
+ ]
+ },
+ "stop_button": {
+ "unicode": "23F9",
+ "unicode_alternates": "",
+ "name": "black square for stop",
+ "shortname": ":stop_button:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sound",
+ "symbol"
+ ]
+ },
+ "stopwatch": {
+ "unicode": "23F1",
+ "unicode_alternates": "",
+ "name": "stopwatch",
+ "shortname": ":stopwatch:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "clock",
+ "object",
+ "time"
+ ]
},
"straight_ruler": {
"unicode": "1F4CF",
@@ -11717,7 +25753,9 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["stationery"],
+ "keywords": [
+ "stationery"
+ ],
"moji": "📏"
},
"strawberry": {
@@ -11728,7 +25766,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "nature", "strawberry", "short", "cake", "berry"],
+ "keywords": [
+ "food",
+ "fruit",
+ "nature",
+ "strawberry",
+ "short",
+ "cake",
+ "berry"
+ ],
"moji": "🍓"
},
"stuck_out_tongue": {
@@ -11738,8 +25784,32 @@
"shortname": ":stuck_out_tongue:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [":P", ":-P", "=P", ":-p", ":p", "=p", ":-Þ", ":Þ", ":þ", ":-þ", ":-b", ":b", "d:"],
- "keywords": ["childish", "face", "mischievous", "playful", "prank", "tongue", "silly", "playful", "cheeky"],
+ "aliases_ascii": [
+ ":P",
+ ":-P",
+ "=P",
+ ":-p",
+ ":p",
+ "=p",
+ ":-Þ",
+ ":Þ",
+ ":þ",
+ ":-þ",
+ ":-b",
+ ":b",
+ "d:"
+ ],
+ "keywords": [
+ "childish",
+ "face",
+ "mischievous",
+ "playful",
+ "prank",
+ "tongue",
+ "silly",
+ "playful",
+ "cheeky"
+ ],
"moji": "😛"
},
"stuck_out_tongue_closed_eyes": {
@@ -11750,7 +25820,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "mischievous", "playful", "prank", "tongue", "kidding", "silly", "playful", "ecstatic"],
+ "keywords": [
+ "face",
+ "mischievous",
+ "playful",
+ "prank",
+ "tongue",
+ "kidding",
+ "silly",
+ "playful",
+ "ecstatic"
+ ],
"moji": "😝"
},
"stuck_out_tongue_winking_eye": {
@@ -11760,8 +25840,25 @@
"shortname": ":stuck_out_tongue_winking_eye:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [">:P", "X-P", "x-p"],
- "keywords": ["childish", "face", "mischievous", "playful", "prank", "tongue", "wink", "winking", "kidding", "silly", "playful", "crazy"],
+ "aliases_ascii": [
+ ">:P",
+ "X-P",
+ "x-p"
+ ],
+ "keywords": [
+ "childish",
+ "face",
+ "mischievous",
+ "playful",
+ "prank",
+ "tongue",
+ "wink",
+ "winking",
+ "kidding",
+ "silly",
+ "playful",
+ "crazy"
+ ],
"moji": "😜"
},
"sun_with_face": {
@@ -11772,7 +25869,13 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["morning", "sun", "anthropomorphic", "face", "sky"],
+ "keywords": [
+ "morning",
+ "sun",
+ "anthropomorphic",
+ "face",
+ "sky"
+ ],
"moji": "🌞"
},
"sunflower": {
@@ -11783,7 +25886,15 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "plant", "sunflower", "sun", "flower", "seeds", "yellow"],
+ "keywords": [
+ "nature",
+ "plant",
+ "sunflower",
+ "sun",
+ "flower",
+ "seeds",
+ "yellow"
+ ],
"moji": "🌻"
},
"sunglasses": {
@@ -11793,19 +25904,41 @@
"shortname": ":sunglasses:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["B-)", "B)", "8)", "8-)", "B-D", "8-D"],
- "keywords": ["cool", "face", "smiling", "sunglasses", "sun", "glasses", "sunny", "cool", "smooth"],
+ "aliases_ascii": [
+ "B-)",
+ "B)",
+ "8)",
+ "8-)",
+ "B-D",
+ "8-D"
+ ],
+ "keywords": [
+ "cool",
+ "face",
+ "smiling",
+ "sunglasses",
+ "sun",
+ "glasses",
+ "sunny",
+ "cool",
+ "smooth"
+ ],
"moji": "😎"
},
"sunny": {
"unicode": "2600",
- "unicode_alternates": ["2600-FE0F"],
+ "unicode_alternates": [
+ "2600-FE0F"
+ ],
"name": "black sun with rays",
"shortname": ":sunny:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["brightness", "weather"]
+ "keywords": [
+ "brightness",
+ "weather"
+ ]
},
"sunrise": {
"unicode": "1F305",
@@ -11815,7 +25948,17 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["morning", "photo", "vacation", "view", "sunrise", "sun", "morning", "color", "sky"],
+ "keywords": [
+ "morning",
+ "photo",
+ "vacation",
+ "view",
+ "sunrise",
+ "sun",
+ "morning",
+ "color",
+ "sky"
+ ],
"moji": "🌅"
},
"sunrise_over_mountains": {
@@ -11826,7 +25969,18 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["photo", "vacation", "view", "sunrise", "sun", "morning", "mountain", "rural", "color", "sky"],
+ "keywords": [
+ "photo",
+ "vacation",
+ "view",
+ "sunrise",
+ "sun",
+ "morning",
+ "mountain",
+ "rural",
+ "color",
+ "sky"
+ ],
"moji": "🌄"
},
"surfer": {
@@ -11837,9 +25991,114 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["ocean", "sea", "sports", "surfer", "surf", "wave", "ocean", "ride", "swell"],
+ "keywords": [
+ "ocean",
+ "sea",
+ "sports",
+ "surfer",
+ "surf",
+ "wave",
+ "ocean",
+ "ride",
+ "swell"
+ ],
"moji": "🏄"
},
+ "surfer_tone1": {
+ "unicode": "1F3C4-1F3FB",
+ "unicode_alternates": "",
+ "name": "surfer tone 1",
+ "shortname": ":surfer_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "ocean",
+ "sea",
+ "sport",
+ "surf",
+ "wave",
+ "ocean",
+ "ride",
+ "swell"
+ ]
+ },
+ "surfer_tone2": {
+ "unicode": "1F3C4-1F3FC",
+ "unicode_alternates": "",
+ "name": "surfer tone 2",
+ "shortname": ":surfer_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "ocean",
+ "sea",
+ "sport",
+ "surf",
+ "wave",
+ "ocean",
+ "ride",
+ "swell"
+ ]
+ },
+ "surfer_tone3": {
+ "unicode": "1F3C4-1F3FD",
+ "unicode_alternates": "",
+ "name": "surfer tone 3",
+ "shortname": ":surfer_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "ocean",
+ "sea",
+ "sport",
+ "surf",
+ "wave",
+ "ocean",
+ "ride",
+ "swell"
+ ]
+ },
+ "surfer_tone4": {
+ "unicode": "1F3C4-1F3FE",
+ "unicode_alternates": "",
+ "name": "surfer tone 4",
+ "shortname": ":surfer_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "ocean",
+ "sea",
+ "sport",
+ "surf",
+ "wave",
+ "ocean",
+ "ride",
+ "swell"
+ ]
+ },
+ "surfer_tone5": {
+ "unicode": "1F3C4-1F3FF",
+ "unicode_alternates": "",
+ "name": "surfer tone 5",
+ "shortname": ":surfer_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "ocean",
+ "sea",
+ "sport",
+ "surf",
+ "wave",
+ "ocean",
+ "ride",
+ "swell"
+ ]
+ },
"sushi": {
"unicode": "1F363",
"unicode_alternates": [],
@@ -11848,7 +26107,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "japanese", "sushi", "fish", "raw", "nigiri", "japanese"],
+ "keywords": [
+ "food",
+ "japanese",
+ "sushi",
+ "fish",
+ "raw",
+ "nigiri",
+ "japanese"
+ ],
"moji": "🍣"
},
"suspension_railway": {
@@ -11859,7 +26126,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "suspension", "railway", "rail", "train", "transportation"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "suspension",
+ "railway",
+ "rail",
+ "train",
+ "transportation"
+ ],
"moji": "🚟"
},
"sweat": {
@@ -11869,8 +26144,22 @@
"shortname": ":sweat:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["':(", "':-(", "'=("],
- "keywords": ["cold", "sweat", "sick", "anxious", "worried", "clammy", "diaphoresis", "face", "hot"],
+ "aliases_ascii": [
+ "':(",
+ "':-(",
+ "'=("
+ ],
+ "keywords": [
+ "cold",
+ "sweat",
+ "sick",
+ "anxious",
+ "worried",
+ "clammy",
+ "diaphoresis",
+ "face",
+ "hot"
+ ],
"moji": "😓"
},
"sweat_drops": {
@@ -11881,7 +26170,9 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["water"],
+ "keywords": [
+ "water"
+ ],
"moji": "💦"
},
"sweat_smile": {
@@ -11891,8 +26182,23 @@
"shortname": ":sweat_smile:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": ["':)", "':-)", "'=)", "':D", "':-D", "'=D"],
- "keywords": ["face", "happy", "hot", "smiling", "cold", "sweat", "perspiration"],
+ "aliases_ascii": [
+ "':)",
+ "':-)",
+ "'=)",
+ "':D",
+ "':-D",
+ "'=D"
+ ],
+ "keywords": [
+ "face",
+ "happy",
+ "hot",
+ "smiling",
+ "cold",
+ "sweat",
+ "perspiration"
+ ],
"moji": "😅"
},
"sweet_potato": {
@@ -11903,7 +26209,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "nature", "sweet", "potato", "potassium", "roasted", "roast"],
+ "keywords": [
+ "food",
+ "nature",
+ "sweet",
+ "potato",
+ "potassium",
+ "roasted",
+ "roast"
+ ],
"moji": "🍠"
},
"swimmer": {
@@ -11914,9 +26228,120 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sports", "swimmer", "swim", "water", "pool", "laps", "freestyle", "butterfly", "breaststroke", "backstroke"],
+ "keywords": [
+ "sports",
+ "swimmer",
+ "swim",
+ "water",
+ "pool",
+ "laps",
+ "freestyle",
+ "butterfly",
+ "breaststroke",
+ "backstroke"
+ ],
"moji": "🏊"
},
+ "swimmer_tone1": {
+ "unicode": "1F3CA-1F3FB",
+ "unicode_alternates": "",
+ "name": "swimmer tone 1",
+ "shortname": ":swimmer_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "swim",
+ "water",
+ "pool",
+ "laps",
+ "freestyle",
+ "butterfly",
+ "breaststroke",
+ "backstroke"
+ ]
+ },
+ "swimmer_tone2": {
+ "unicode": "1F3CA-1F3FC",
+ "unicode_alternates": "",
+ "name": "swimmer tone 2",
+ "shortname": ":swimmer_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "swim",
+ "water",
+ "pool",
+ "laps",
+ "freestyle",
+ "butterfly",
+ "breaststroke",
+ "backstroke"
+ ]
+ },
+ "swimmer_tone3": {
+ "unicode": "1F3CA-1F3FD",
+ "unicode_alternates": "",
+ "name": "swimmer tone 3",
+ "shortname": ":swimmer_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "swim",
+ "water",
+ "pool",
+ "laps",
+ "freestyle",
+ "butterfly",
+ "breaststroke",
+ "backstroke"
+ ]
+ },
+ "swimmer_tone4": {
+ "unicode": "1F3CA-1F3FE",
+ "unicode_alternates": "",
+ "name": "swimmer tone 4",
+ "shortname": ":swimmer_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "swim",
+ "water",
+ "pool",
+ "laps",
+ "freestyle",
+ "butterfly",
+ "breaststroke",
+ "backstroke"
+ ]
+ },
+ "swimmer_tone5": {
+ "unicode": "1F3CA-1F3FF",
+ "unicode_alternates": "",
+ "name": "swimmer tone 5",
+ "shortname": ":swimmer_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "sport",
+ "swim",
+ "water",
+ "pool",
+ "laps",
+ "freestyle",
+ "butterfly",
+ "breaststroke",
+ "backstroke"
+ ]
+ },
"symbols": {
"unicode": "1F523",
"unicode_alternates": [],
@@ -11925,9 +26350,21 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "🔣"
},
+ "synagogue": {
+ "unicode": "1F54D",
+ "unicode_alternates": "",
+ "name": "synagogue",
+ "shortname": ":synagogue:",
+ "category": "travel",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"syringe": {
"unicode": "1F489",
"unicode_alternates": [],
@@ -11936,9 +26373,26 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blood", "drugs", "health", "hospital", "medicine", "needle"],
+ "keywords": [
+ "blood",
+ "drugs",
+ "health",
+ "hospital",
+ "medicine",
+ "needle"
+ ],
"moji": "💉"
},
+ "taco": {
+ "unicode": "1F32E",
+ "unicode_alternates": "",
+ "name": "taco",
+ "shortname": ":taco:",
+ "category": "foods",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"tada": {
"unicode": "1F389",
"unicode_alternates": [],
@@ -11947,7 +26401,18 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["contulations", "party", "party", "popper", "tada", "celebration", "victory", "announcement", "climax", "congratulations"],
+ "keywords": [
+ "contulations",
+ "party",
+ "party",
+ "popper",
+ "tada",
+ "celebration",
+ "victory",
+ "announcement",
+ "climax",
+ "congratulations"
+ ],
"moji": "🎉"
},
"tanabata_tree": {
@@ -11958,7 +26423,16 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "plant", "tanabata", "tree", "festival", "star", "wish", "holiday"],
+ "keywords": [
+ "nature",
+ "plant",
+ "tanabata",
+ "tree",
+ "festival",
+ "star",
+ "wish",
+ "holiday"
+ ],
"moji": "🎋"
},
"tangerine": {
@@ -11969,18 +26443,40 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "nature", "tangerine", "citrus", "orange"],
+ "keywords": [
+ "food",
+ "fruit",
+ "nature",
+ "tangerine",
+ "citrus",
+ "orange"
+ ],
"moji": "🍊"
},
"taurus": {
"unicode": "2649",
- "unicode_alternates": ["2649-FE0F"],
+ "unicode_alternates": [
+ "2649-FE0F"
+ ],
"name": "taurus",
"shortname": ":taurus:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["purple-square", "sign", "taurus", "bull", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "purple-square",
+ "sign",
+ "taurus",
+ "bull",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♉"
},
"taxi": {
@@ -11991,7 +26487,18 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cars", "transportation", "uber", "vehicle", "taxi", "car", "automobile", "city", "transport", "service"],
+ "keywords": [
+ "cars",
+ "transportation",
+ "uber",
+ "vehicle",
+ "taxi",
+ "car",
+ "automobile",
+ "city",
+ "transport",
+ "service"
+ ],
"moji": "🚕"
},
"tea": {
@@ -12002,18 +26509,36 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bowl", "breakfast", "british", "drink", "green", "tea", "leaf", "drink", "teacup", "hot", "beverage"],
+ "keywords": [
+ "bowl",
+ "breakfast",
+ "british",
+ "drink",
+ "green",
+ "tea",
+ "leaf",
+ "drink",
+ "teacup",
+ "hot",
+ "beverage"
+ ],
"moji": "🍵"
},
"telephone": {
"unicode": "260E",
- "unicode_alternates": ["260E-FE0F"],
+ "unicode_alternates": [
+ "260E-FE0F"
+ ],
"name": "black telephone",
"shortname": ":telephone:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "dial", "technology"],
+ "keywords": [
+ "communication",
+ "dial",
+ "technology"
+ ],
"moji": "☎"
},
"telephone_black": {
@@ -12022,9 +26547,15 @@
"name": "black touchtone telephone",
"shortname": ":telephone_black:",
"category": "objects_symbols",
- "aliases": [":black_touchtone_telephone:"],
+ "aliases": [
+ ":black_touchtone_telephone:"
+ ],
"aliases_ascii": [],
- "keywords": ["communication", "dial", "technology"]
+ "keywords": [
+ "communication",
+ "dial",
+ "technology"
+ ]
},
"telephone_receiver": {
"unicode": "1F4DE",
@@ -12034,7 +26565,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["communication", "dial", "technology"],
+ "keywords": [
+ "communication",
+ "dial",
+ "technology"
+ ],
"moji": "📞"
},
"telephone_white": {
@@ -12043,9 +26578,15 @@
"name": "white touchtone telephone",
"shortname": ":telephone_white:",
"category": "objects_symbols",
- "aliases": [":white_touchtone_telephone:"],
+ "aliases": [
+ ":white_touchtone_telephone:"
+ ],
"aliases_ascii": [],
- "keywords": ["communication", "dial", "technology"]
+ "keywords": [
+ "communication",
+ "dial",
+ "technology"
+ ]
},
"telescope": {
"unicode": "1F52D",
@@ -12055,9 +26596,28 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["space", "stars"],
+ "keywords": [
+ "space",
+ "stars"
+ ],
"moji": "🔭"
},
+ "ten": {
+ "unicode": "1F51F",
+ "unicode_alternates": "",
+ "name": "keycap ten",
+ "shortname": ":ten:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "10",
+ "blue-square",
+ "numbers",
+ "symbol",
+ "word"
+ ]
+ },
"tennis": {
"unicode": "1F3BE",
"unicode_alternates": [],
@@ -12066,18 +26626,36 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["balls", "green", "sports", "tennis", "racket", "racquet", "ball", "game", "net", "court", "love"],
+ "keywords": [
+ "balls",
+ "green",
+ "sports",
+ "tennis",
+ "racket",
+ "racquet",
+ "ball",
+ "game",
+ "net",
+ "court",
+ "love"
+ ],
"moji": "🎾"
},
"tent": {
"unicode": "26FA",
- "unicode_alternates": ["26FA-FE0F"],
+ "unicode_alternates": [
+ "26FA-FE0F"
+ ],
"name": "tent",
"shortname": ":tent:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["camp", "outdoors", "photo"],
+ "keywords": [
+ "camp",
+ "outdoors",
+ "photo"
+ ],
"moji": "⛺"
},
"thermometer": {
@@ -12088,7 +26666,33 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["temperature"]
+ "keywords": [
+ "temperature"
+ ]
+ },
+ "thermometer_face": {
+ "unicode": "1F912",
+ "unicode_alternates": "",
+ "name": "face with thermometer",
+ "shortname": ":thermometer_face:",
+ "category": "people",
+ "aliases": [
+ ":face_with_thermometer:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "thinking": {
+ "unicode": "1F914",
+ "unicode_alternates": "",
+ "name": "thinking face",
+ "shortname": ":thinking:",
+ "category": "people",
+ "aliases": [
+ ":thinking_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
},
"thought_balloon": {
"unicode": "1F4AD",
@@ -12098,7 +26702,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bubble", "cloud", "speech", "thought", "balloon", "comic", "think", "day dream", "wonder"],
+ "keywords": [
+ "bubble",
+ "cloud",
+ "speech",
+ "thought",
+ "balloon",
+ "comic",
+ "think",
+ "day dream",
+ "wonder"
+ ],
"moji": "💭"
},
"thought_left": {
@@ -12107,9 +26721,18 @@
"name": "left thought bubble",
"shortname": ":thought_left:",
"category": "objects_symbols",
- "aliases": [":left_thought_bubble:"],
- "aliases_ascii": [],
- "keywords": ["balloon", "cloud", "comic", "think", "day dream", "wonder"]
+ "aliases": [
+ ":left_thought_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "balloon",
+ "cloud",
+ "comic",
+ "think",
+ "day dream",
+ "wonder"
+ ]
},
"thought_right": {
"unicode": "1F5ED",
@@ -12117,20 +26740,36 @@
"name": "right thought bubble",
"shortname": ":thought_right:",
"category": "objects_symbols",
- "aliases": [":right_thought_bubble:"],
- "aliases_ascii": [],
- "keywords": ["balloon", "cloud", "comic", "think", "day dream", "wonder"]
+ "aliases": [
+ ":right_thought_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "balloon",
+ "cloud",
+ "comic",
+ "think",
+ "day dream",
+ "wonder"
+ ]
},
"three": {
"moji": "3️⃣",
"unicode": "0033-20E3",
- "unicode_alternates": ["0033-FE0F-20E3"],
+ "unicode_alternates": [
+ "0033-FE0F-20E3"
+ ],
"name": "digit three",
"shortname": ":three:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["3", "blue-square", "numbers", "prime"]
+ "keywords": [
+ "3",
+ "blue-square",
+ "numbers",
+ "prime"
+ ]
},
"thumbs_down_reverse": {
"unicode": "1F593",
@@ -12138,9 +26777,15 @@
"name": "reversed thumbs down sign",
"shortname": ":thumbs_down_reverse:",
"category": "people",
- "aliases": [":reversed_thumbs_down_sign:"],
+ "aliases": [
+ ":reversed_thumbs_down_sign:"
+ ],
"aliases_ascii": [],
- "keywords": ["hand", "no", "-1"]
+ "keywords": [
+ "hand",
+ "no",
+ "-1"
+ ]
},
"thumbs_up_reverse": {
"unicode": "1F592",
@@ -12148,9 +26793,17 @@
"name": "reversed thumbs up sign",
"shortname": ":thumbs_up_reverse:",
"category": "people",
- "aliases": [":reversed_thumbs_up_sign:"],
- "aliases_ascii": [],
- "keywords": ["cool", "hand", "like", "yes", "+1"]
+ "aliases": [
+ ":reversed_thumbs_up_sign:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cool",
+ "hand",
+ "like",
+ "yes",
+ "+1"
+ ]
},
"thumbsdown": {
"unicode": "1F44E",
@@ -12158,22 +26811,219 @@
"name": "thumbs down sign",
"shortname": ":thumbsdown:",
"category": "emoticons",
- "aliases": [":-1:"],
- "aliases_ascii": [],
- "keywords": ["hand", "no"],
+ "aliases": [
+ ":-1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hand",
+ "no"
+ ],
"moji": "👎"
},
+ "thumbsdown_tone1": {
+ "unicode": "1F44E-1F3FB",
+ "unicode_alternates": "",
+ "name": "thumbs down sign tone 1",
+ "shortname": ":thumbsdown_tone1:",
+ "category": "people",
+ "aliases": [
+ ":-1_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hand",
+ "no",
+ "-1"
+ ]
+ },
+ "thumbsdown_tone2": {
+ "unicode": "1F44E-1F3FC",
+ "unicode_alternates": "",
+ "name": "thumbs down sign tone 2",
+ "shortname": ":thumbsdown_tone2:",
+ "category": "people",
+ "aliases": [
+ ":-1_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hand",
+ "no",
+ "-1"
+ ]
+ },
+ "thumbsdown_tone3": {
+ "unicode": "1F44E-1F3FD",
+ "unicode_alternates": "",
+ "name": "thumbs down sign tone 3",
+ "shortname": ":thumbsdown_tone3:",
+ "category": "people",
+ "aliases": [
+ ":-1_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hand",
+ "no",
+ "-1"
+ ]
+ },
+ "thumbsdown_tone4": {
+ "unicode": "1F44E-1F3FE",
+ "unicode_alternates": "",
+ "name": "thumbs down sign tone 4",
+ "shortname": ":thumbsdown_tone4:",
+ "category": "people",
+ "aliases": [
+ ":-1_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hand",
+ "no",
+ "-1"
+ ]
+ },
+ "thumbsdown_tone5": {
+ "unicode": "1F44E-1F3FF",
+ "unicode_alternates": "",
+ "name": "thumbs down sign tone 5",
+ "shortname": ":thumbsdown_tone5:",
+ "category": "people",
+ "aliases": [
+ ":-1_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "hand",
+ "no",
+ "-1"
+ ]
+ },
"thumbsup": {
"unicode": "1F44D",
"unicode_alternates": [],
"name": "thumbs up sign",
"shortname": ":thumbsup:",
"category": "emoticons",
- "aliases": [":+1:"],
- "aliases_ascii": [],
- "keywords": ["cool", "hand", "like", "yes"],
+ "aliases": [
+ ":+1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cool",
+ "hand",
+ "like",
+ "yes"
+ ],
"moji": "👍"
},
+ "thumbsup_tone1": {
+ "unicode": "1F44D-1F3FB",
+ "unicode_alternates": "",
+ "name": "thumbs up sign tone 1",
+ "shortname": ":thumbsup_tone1:",
+ "category": "people",
+ "aliases": [
+ ":+1_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cool",
+ "hand",
+ "like",
+ "yes",
+ "+1"
+ ]
+ },
+ "thumbsup_tone2": {
+ "unicode": "1F44D-1F3FC",
+ "unicode_alternates": "",
+ "name": "thumbs up sign tone 2",
+ "shortname": ":thumbsup_tone2:",
+ "category": "people",
+ "aliases": [
+ ":+1_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cool",
+ "hand",
+ "like",
+ "yes",
+ "+1"
+ ]
+ },
+ "thumbsup_tone3": {
+ "unicode": "1F44D-1F3FD",
+ "unicode_alternates": "",
+ "name": "thumbs up sign tone 3",
+ "shortname": ":thumbsup_tone3:",
+ "category": "people",
+ "aliases": [
+ ":+1_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cool",
+ "hand",
+ "like",
+ "yes",
+ "+1"
+ ]
+ },
+ "thumbsup_tone4": {
+ "unicode": "1F44D-1F3FE",
+ "unicode_alternates": "",
+ "name": "thumbs up sign tone 4",
+ "shortname": ":thumbsup_tone4:",
+ "category": "people",
+ "aliases": [
+ ":+1_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cool",
+ "hand",
+ "like",
+ "yes",
+ "+1"
+ ]
+ },
+ "thumbsup_tone5": {
+ "unicode": "1F44D-1F3FF",
+ "unicode_alternates": "",
+ "name": "thumbs up sign tone 5",
+ "shortname": ":thumbsup_tone5:",
+ "category": "people",
+ "aliases": [
+ ":+1_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "cool",
+ "hand",
+ "like",
+ "yes",
+ "+1"
+ ]
+ },
+ "thunder_cloud_rain": {
+ "unicode": "26C8",
+ "unicode_alternates": "",
+ "name": "thunder cloud and rain",
+ "shortname": ":thunder_cloud_rain:",
+ "category": "nature",
+ "aliases": [
+ ":thunder_cloud_and_rain:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "nature",
+ "weather"
+ ]
+ },
"ticket": {
"unicode": "1F3AB",
"unicode_alternates": [],
@@ -12182,7 +27032,18 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["concert", "event", "pass", "ticket", "show", "entertainment", "stub", "admission", "proof", "purchase"],
+ "keywords": [
+ "concert",
+ "event",
+ "pass",
+ "ticket",
+ "show",
+ "entertainment",
+ "stub",
+ "admission",
+ "proof",
+ "purchase"
+ ],
"moji": "🎫"
},
"tickets": {
@@ -12191,9 +27052,20 @@
"name": "admission tickets",
"shortname": ":tickets:",
"category": "activity",
- "aliases": [":admission_tickets:"],
- "aliases_ascii": [],
- "keywords": ["concert", "event", "pass", "show", "entertainment", "stub", "proof", "purchase"]
+ "aliases": [
+ ":admission_tickets:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "concert",
+ "event",
+ "pass",
+ "show",
+ "entertainment",
+ "stub",
+ "proof",
+ "purchase"
+ ]
},
"tiger": {
"unicode": "1F42F",
@@ -12203,7 +27075,9 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal"],
+ "keywords": [
+ "animal"
+ ],
"moji": "🐯"
},
"tiger2": {
@@ -12214,9 +27088,33 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "tiger", "cat", "striped", "tony", "tigger", "hobs"],
+ "keywords": [
+ "animal",
+ "nature",
+ "tiger",
+ "cat",
+ "striped",
+ "tony",
+ "tigger",
+ "hobs"
+ ],
"moji": "🐅"
},
+ "timer": {
+ "unicode": "23F2",
+ "unicode_alternates": "",
+ "name": "timer clock",
+ "shortname": ":timer:",
+ "category": "objects",
+ "aliases": [
+ ":timer_clock:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "object",
+ "time"
+ ]
+ },
"tired_face": {
"unicode": "1F62B",
"unicode_alternates": [],
@@ -12225,9 +27123,34 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "frustrated", "sick", "upset", "whine", "exhausted", "sleepy", "tired"],
+ "keywords": [
+ "face",
+ "frustrated",
+ "sick",
+ "upset",
+ "whine",
+ "exhausted",
+ "sleepy",
+ "tired"
+ ],
"moji": "😫"
},
+ "tm": {
+ "unicode": "2122",
+ "unicode_alternates": "2122-fe0f",
+ "name": "trade mark sign",
+ "shortname": ":tm:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "brand",
+ "trademark",
+ "symbol",
+ "tm",
+ "word"
+ ]
+ },
"toilet": {
"unicode": "1F6BD",
"unicode_alternates": [],
@@ -12236,7 +27159,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["restroom", "wc", "toilet", "bathroom", "throne", "porcelain", "waste", "flush", "plumbing"],
+ "keywords": [
+ "restroom",
+ "wc",
+ "toilet",
+ "bathroom",
+ "throne",
+ "porcelain",
+ "waste",
+ "flush",
+ "plumbing"
+ ],
"moji": "🚽"
},
"tokyo_tower": {
@@ -12247,7 +27180,10 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["japan", "photo"],
+ "keywords": [
+ "japan",
+ "photo"
+ ],
"moji": "🗼"
},
"tomato": {
@@ -12258,9 +27194,68 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "nature", "vegetable", "tomato", "fruit", "sauce", "italian"],
+ "keywords": [
+ "food",
+ "fruit",
+ "nature",
+ "vegetable",
+ "tomato",
+ "fruit",
+ "sauce",
+ "italian"
+ ],
"moji": "🍅"
},
+ "tone1": {
+ "unicode": "1F3FB",
+ "unicode_alternates": "",
+ "name": "emoji modifier Fitzpatrick type-1-2",
+ "shortname": ":tone1:",
+ "category": "modifier",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "tone2": {
+ "unicode": "1F3FC",
+ "unicode_alternates": "",
+ "name": "emoji modifier Fitzpatrick type-3",
+ "shortname": ":tone2:",
+ "category": "modifier",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "tone3": {
+ "unicode": "1F3FD",
+ "unicode_alternates": "",
+ "name": "emoji modifier Fitzpatrick type-4",
+ "shortname": ":tone3:",
+ "category": "modifier",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "tone4": {
+ "unicode": "1F3FE",
+ "unicode_alternates": "",
+ "name": "emoji modifier Fitzpatrick type-5",
+ "shortname": ":tone4:",
+ "category": "modifier",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "tone5": {
+ "unicode": "1F3FF",
+ "unicode_alternates": "",
+ "name": "emoji modifier Fitzpatrick type-6",
+ "shortname": ":tone5:",
+ "category": "modifier",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"tongue": {
"unicode": "1F445",
"unicode_alternates": [],
@@ -12269,7 +27264,25 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mouth", "playful", "tongue", "mouth", "taste", "buds", "food", "silly", "playful", "tease", "kiss", "french kiss", "lick", "tasty", "playfulness", "silliness", "intimacy"],
+ "keywords": [
+ "mouth",
+ "playful",
+ "tongue",
+ "mouth",
+ "taste",
+ "buds",
+ "food",
+ "silly",
+ "playful",
+ "tease",
+ "kiss",
+ "french kiss",
+ "lick",
+ "tasty",
+ "playfulness",
+ "silliness",
+ "intimacy"
+ ],
"moji": "👅"
},
"tools": {
@@ -12278,9 +27291,13 @@
"name": "hammer and wrench",
"shortname": ":tools:",
"category": "objects_symbols",
- "aliases": [":hammer_and_wrench:"],
+ "aliases": [
+ ":hammer_and_wrench:"
+ ],
"aliases_ascii": [],
- "keywords": ["tools"]
+ "keywords": [
+ "tools"
+ ]
},
"top": {
"unicode": "1F51D",
@@ -12290,7 +27307,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "words"],
+ "keywords": [
+ "blue-square",
+ "words"
+ ],
"moji": "🔝"
},
"tophat": {
@@ -12301,9 +27321,63 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["classy", "gentleman", "magic", "top", "hat", "cap", "beaver", "high", "tall", "stove", "pipe", "chimney", "topper", "london", "period piece", "magic", "magician"],
+ "keywords": [
+ "classy",
+ "gentleman",
+ "magic",
+ "top",
+ "hat",
+ "cap",
+ "beaver",
+ "high",
+ "tall",
+ "stove",
+ "pipe",
+ "chimney",
+ "topper",
+ "london",
+ "period piece",
+ "magic",
+ "magician"
+ ],
"moji": "🎩"
},
+ "track_next": {
+ "unicode": "23ED",
+ "unicode_alternates": "",
+ "name": "black right-pointing double triangle with vertical bar",
+ "shortname": ":track_next:",
+ "category": "symbols",
+ "aliases": [
+ ":next_track:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "arrow",
+ "next scene",
+ "next track",
+ "sound",
+ "symbol"
+ ]
+ },
+ "track_previous": {
+ "unicode": "23EE",
+ "unicode_alternates": "",
+ "name": "black left-pointing double triangle with vertical bar",
+ "shortname": ":track_previous:",
+ "category": "symbols",
+ "aliases": [
+ ":previous_track:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "arrow",
+ "previous scene",
+ "previous track",
+ "sound",
+ "symbol"
+ ]
+ },
"trackball": {
"unicode": "1F5B2",
"unicode_alternates": [],
@@ -12312,7 +27386,11 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["input", "device", "gadget"]
+ "keywords": [
+ "input",
+ "device",
+ "gadget"
+ ]
},
"tractor": {
"unicode": "1F69C",
@@ -12322,7 +27400,17 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["agriculture", "car", "farming", "vehicle", "tractor", "farm", "construction", "machine", "digger"],
+ "keywords": [
+ "agriculture",
+ "car",
+ "farming",
+ "vehicle",
+ "tractor",
+ "farm",
+ "construction",
+ "machine",
+ "digger"
+ ],
"moji": "🚜"
},
"traffic_light": {
@@ -12333,7 +27421,16 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["traffic", "transportation", "traffic", "light", "stop", "go", "yield", "horizontal"],
+ "keywords": [
+ "traffic",
+ "transportation",
+ "traffic",
+ "light",
+ "stop",
+ "go",
+ "yield",
+ "horizontal"
+ ],
"moji": "🚥"
},
"train": {
@@ -12344,7 +27441,10 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["tram", "rail"]
+ "keywords": [
+ "tram",
+ "rail"
+ ]
},
"train2": {
"unicode": "1F686",
@@ -12354,7 +27454,13 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "train", "locomotive", "rail"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "train",
+ "locomotive",
+ "rail"
+ ],
"moji": "🚆"
},
"train_diesel": {
@@ -12363,9 +27469,16 @@
"name": "diesel locomotive",
"shortname": ":train_diesel:",
"category": "travel_places",
- "aliases": [":diesel_locomotive:"],
+ "aliases": [
+ ":diesel_locomotive:"
+ ],
"aliases_ascii": [],
- "keywords": ["train", "transportation", "engine", "rail"]
+ "keywords": [
+ "train",
+ "transportation",
+ "engine",
+ "rail"
+ ]
},
"tram": {
"unicode": "1F68A",
@@ -12375,7 +27488,13 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "vehicle", "tram", "transportation", "transport"],
+ "keywords": [
+ "transportation",
+ "vehicle",
+ "tram",
+ "transportation",
+ "transport"
+ ],
"moji": "🚊"
},
"triangle_round": {
@@ -12384,9 +27503,15 @@
"name": "triangle with rounded corners",
"shortname": ":triangle_round:",
"category": "objects_symbols",
- "aliases": [":triangle_with_rounded_corners:"],
+ "aliases": [
+ ":triangle_with_rounded_corners:"
+ ],
"aliases_ascii": [],
- "keywords": ["caution", "warning", "alert"]
+ "keywords": [
+ "caution",
+ "warning",
+ "alert"
+ ]
},
"triangular_flag_on_post": {
"unicode": "1F6A9",
@@ -12396,7 +27521,14 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["triangle", "triangular", "flag", "golf", "post", "flagpole"],
+ "keywords": [
+ "triangle",
+ "triangular",
+ "flag",
+ "golf",
+ "post",
+ "flagpole"
+ ],
"moji": "🚩"
},
"triangular_ruler": {
@@ -12407,7 +27539,12 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["architect", "math", "sketch", "stationery"],
+ "keywords": [
+ "architect",
+ "math",
+ "sketch",
+ "stationery"
+ ],
"moji": "📐"
},
"trident": {
@@ -12418,7 +27555,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["spear", "weapon"],
+ "keywords": [
+ "spear",
+ "weapon"
+ ],
"moji": "🔱"
},
"triumph": {
@@ -12429,7 +27569,14 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "gas", "phew", "triumph", "steam", "breath"],
+ "keywords": [
+ "face",
+ "gas",
+ "phew",
+ "triumph",
+ "steam",
+ "breath"
+ ],
"moji": "😤"
},
"trolleybus": {
@@ -12440,7 +27587,16 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bart", "transportation", "vehicle", "trolley", "bus", "city", "transport", "transportation"],
+ "keywords": [
+ "bart",
+ "transportation",
+ "vehicle",
+ "trolley",
+ "bus",
+ "city",
+ "transport",
+ "transportation"
+ ],
"moji": "🚎"
},
"trophy": {
@@ -12451,7 +27607,22 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["award", "ceremony", "contest", "ftw", "place", "win", "trophy", "first", "show", "place", "win", "reward", "achievement", "medal"],
+ "keywords": [
+ "award",
+ "ceremony",
+ "contest",
+ "ftw",
+ "place",
+ "win",
+ "trophy",
+ "first",
+ "show",
+ "place",
+ "win",
+ "reward",
+ "achievement",
+ "medal"
+ ],
"moji": "🏆"
},
"tropical_drink": {
@@ -12462,7 +27633,17 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["beverage", "tropical", "drink", "mixed", "pineapple", "coconut", "pina", "fruit", "umbrella"],
+ "keywords": [
+ "beverage",
+ "tropical",
+ "drink",
+ "mixed",
+ "pineapple",
+ "coconut",
+ "pina",
+ "fruit",
+ "umbrella"
+ ],
"moji": "🍹"
},
"tropical_fish": {
@@ -12473,7 +27654,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "swim"],
+ "keywords": [
+ "animal",
+ "swim"
+ ],
"moji": "🐠"
},
"truck": {
@@ -12484,7 +27668,13 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["cars", "transportation", "truck", "delivery", "package"],
+ "keywords": [
+ "cars",
+ "transportation",
+ "truck",
+ "delivery",
+ "package"
+ ],
"moji": "🚚"
},
"trumpet": {
@@ -12495,7 +27685,14 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["brass", "music", "trumpet", "brass", "music", "instrument"],
+ "keywords": [
+ "brass",
+ "music",
+ "trumpet",
+ "brass",
+ "music",
+ "instrument"
+ ],
"moji": "🎺"
},
"tulip": {
@@ -12506,18 +27703,42 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["flowers", "nature", "plant", "tulip", "flower", "bulb", "spring", "easter"],
+ "keywords": [
+ "flowers",
+ "nature",
+ "plant",
+ "tulip",
+ "flower",
+ "bulb",
+ "spring",
+ "easter"
+ ],
"moji": "🌷"
},
+ "turkey": {
+ "unicode": "1F983",
+ "unicode_alternates": "",
+ "name": "turkey",
+ "shortname": ":turkey:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"turned_ok_hand": {
"unicode": "1F58F",
"unicode_alternates": [],
"name": "turned ok hand sign",
"shortname": ":turned_ok_hand:",
"category": "people",
- "aliases": [":turned_ok_hand_sign:"],
+ "aliases": [
+ ":turned_ok_hand_sign:"
+ ],
"aliases_ascii": [],
- "keywords": ["perfect", "okay"]
+ "keywords": [
+ "perfect",
+ "okay"
+ ]
},
"turtle": {
"unicode": "1F422",
@@ -12527,9 +27748,39 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "slow", "turtle", "shell", "tortoise", "chelonian", "reptile", "slow", "snap", "steady"],
+ "keywords": [
+ "animal",
+ "slow",
+ "turtle",
+ "shell",
+ "tortoise",
+ "chelonian",
+ "reptile",
+ "slow",
+ "snap",
+ "steady"
+ ],
"moji": "🐢"
},
+ "tv": {
+ "unicode": "1F4FA",
+ "unicode_alternates": "",
+ "name": "television",
+ "shortname": ":tv:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "oldschool",
+ "program",
+ "show",
+ "technology",
+ "tv",
+ "entertainment",
+ "object",
+ "video"
+ ]
+ },
"twisted_rightwards_arrows": {
"unicode": "1F500",
"unicode_alternates": [],
@@ -12538,19 +27789,28 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "🔀"
},
"two": {
"moji": "2️⃣",
"unicode": "0032-20E3",
- "unicode_alternates": ["0032-FE0F-20E3"],
+ "unicode_alternates": [
+ "0032-FE0F-20E3"
+ ],
"name": "digit two",
"shortname": ":two:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["2", "blue-square", "numbers", "prime"]
+ "keywords": [
+ "2",
+ "blue-square",
+ "numbers",
+ "prime"
+ ]
},
"two_hearts": {
"unicode": "1F495",
@@ -12560,7 +27820,17 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines", "heart", "hearts", "two", "love", "emotion"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines",
+ "heart",
+ "hearts",
+ "two",
+ "love",
+ "emotion"
+ ],
"moji": "💕"
},
"two_men_holding_hands": {
@@ -12571,7 +27841,21 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bromance", "couple", "friends", "like", "love", "men", "gay", "homosexual", "friends", "hands", "holding", "team", "unity"],
+ "keywords": [
+ "bromance",
+ "couple",
+ "friends",
+ "like",
+ "love",
+ "men",
+ "gay",
+ "homosexual",
+ "friends",
+ "hands",
+ "holding",
+ "team",
+ "unity"
+ ],
"moji": "👬"
},
"two_women_holding_hands": {
@@ -12582,7 +27866,24 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["couple", "female", "friends", "like", "love", "women", "hands", "girlfriends", "friends", "sisters", "mother", "daughter", "gay", "homosexual", "couple", "unity"],
+ "keywords": [
+ "couple",
+ "female",
+ "friends",
+ "like",
+ "love",
+ "women",
+ "hands",
+ "girlfriends",
+ "friends",
+ "sisters",
+ "mother",
+ "daughter",
+ "gay",
+ "homosexual",
+ "couple",
+ "unity"
+ ],
"moji": "👭"
},
"u5272": {
@@ -12593,7 +27894,13 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "cut", "divide", "kanji", "pink"],
+ "keywords": [
+ "chinese",
+ "cut",
+ "divide",
+ "kanji",
+ "pink"
+ ],
"moji": "🈹"
},
"u5408": {
@@ -12604,7 +27911,12 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "japanese", "join", "kanji"],
+ "keywords": [
+ "chinese",
+ "japanese",
+ "join",
+ "kanji"
+ ],
"moji": "🈴"
},
"u55b6": {
@@ -12615,18 +27927,28 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["japanese", "opening hours"],
+ "keywords": [
+ "japanese",
+ "opening hours"
+ ],
"moji": "🈺"
},
"u6307": {
"unicode": "1F22F",
- "unicode_alternates": ["1F22F-FE0F"],
+ "unicode_alternates": [
+ "1F22F-FE0F"
+ ],
"name": "squared cjk unified ideograph-6307",
"shortname": ":u6307:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "green-square", "kanji", "point"],
+ "keywords": [
+ "chinese",
+ "green-square",
+ "kanji",
+ "point"
+ ],
"moji": "🈯"
},
"u6708": {
@@ -12637,7 +27959,13 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "japanese", "kanji", "moon", "orange-square"],
+ "keywords": [
+ "chinese",
+ "japanese",
+ "kanji",
+ "moon",
+ "orange-square"
+ ],
"moji": "🈷"
},
"u6709": {
@@ -12648,7 +27976,12 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "have", "kanji", "orange-square"],
+ "keywords": [
+ "chinese",
+ "have",
+ "kanji",
+ "orange-square"
+ ],
"moji": "🈶"
},
"u6e80": {
@@ -12659,18 +27992,33 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "full", "japanese", "kanji", "red-square"],
+ "keywords": [
+ "chinese",
+ "full",
+ "japanese",
+ "kanji",
+ "red-square"
+ ],
"moji": "🈵"
},
"u7121": {
"unicode": "1F21A",
- "unicode_alternates": ["1F21A-FE0F"],
+ "unicode_alternates": [
+ "1F21A-FE0F"
+ ],
"name": "squared cjk unified ideograph-7121",
"shortname": ":u7121:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "japanese", "kanji", "no", "nothing", "orange-square"],
+ "keywords": [
+ "chinese",
+ "japanese",
+ "kanji",
+ "no",
+ "nothing",
+ "orange-square"
+ ],
"moji": "🈚"
},
"u7533": {
@@ -12681,7 +28029,11 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "japanese", "kanji"],
+ "keywords": [
+ "chinese",
+ "japanese",
+ "kanji"
+ ],
"moji": "🈸"
},
"u7981": {
@@ -12692,7 +28044,14 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "forbidden", "japanese", "kanji", "limit", "restricted"],
+ "keywords": [
+ "chinese",
+ "forbidden",
+ "japanese",
+ "kanji",
+ "limit",
+ "restricted"
+ ],
"moji": "🈲"
},
"u7a7a": {
@@ -12703,20 +28062,45 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["chinese", "empty", "japanese", "kanji"],
+ "keywords": [
+ "chinese",
+ "empty",
+ "japanese",
+ "kanji"
+ ],
"moji": "🈳"
},
"umbrella": {
"unicode": "2614",
- "unicode_alternates": ["2614-FE0F"],
+ "unicode_alternates": [
+ "2614-FE0F"
+ ],
"name": "umbrella with rain drops",
"shortname": ":umbrella:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["rain", "weather"],
+ "keywords": [
+ "rain",
+ "weather"
+ ],
"moji": "☔"
},
+ "umbrella2": {
+ "unicode": "2602",
+ "unicode_alternates": "",
+ "name": "umbrella",
+ "shortname": ":umbrella2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "clothing",
+ "nature",
+ "rain",
+ "weather"
+ ]
+ },
"unamused": {
"unicode": "1F612",
"unicode_alternates": [],
@@ -12725,7 +28109,19 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["bored", "face", "indifference", "serious", "straight face", "unamused", "not amused", "depressed", "unhappy", "disapprove", "lame"],
+ "keywords": [
+ "bored",
+ "face",
+ "indifference",
+ "serious",
+ "straight face",
+ "unamused",
+ "not amused",
+ "depressed",
+ "unhappy",
+ "disapprove",
+ "lame"
+ ],
"moji": "😒"
},
"underage": {
@@ -12736,9 +28132,26 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["18", "drink", "night", "pub"],
+ "keywords": [
+ "18",
+ "drink",
+ "night",
+ "pub"
+ ],
"moji": "🔞"
},
+ "unicorn": {
+ "unicode": "1F984",
+ "unicode_alternates": "",
+ "name": "unicorn face",
+ "shortname": ":unicorn:",
+ "category": "nature",
+ "aliases": [
+ ":unicorn_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"unlock": {
"unicode": "1F513",
"unicode_alternates": [],
@@ -12747,7 +28160,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["privacy", "security"],
+ "keywords": [
+ "privacy",
+ "security"
+ ],
"moji": "🔓"
},
"up": {
@@ -12758,20 +28174,138 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square"],
+ "keywords": [
+ "blue-square"
+ ],
"moji": "🆙"
},
+ "upside_down": {
+ "unicode": "1F643",
+ "unicode_alternates": "",
+ "name": "upside-down face",
+ "shortname": ":upside_down:",
+ "category": "people",
+ "aliases": [
+ ":upside_down_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
+ },
+ "urn": {
+ "unicode": "26B1",
+ "unicode_alternates": "",
+ "name": "funeral urn",
+ "shortname": ":urn:",
+ "category": "objects",
+ "aliases": [
+ ":funeral_urn:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "death",
+ "object"
+ ]
+ },
"v": {
"unicode": "270C",
- "unicode_alternates": ["270C-FE0F"],
+ "unicode_alternates": [
+ "270C-FE0F"
+ ],
"name": "victory hand",
"shortname": ":v:",
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fingers", "hand", "ohyeah", "peace", "two", "victory"],
+ "keywords": [
+ "fingers",
+ "hand",
+ "ohyeah",
+ "peace",
+ "two",
+ "victory"
+ ],
"moji": "✌"
},
+ "v_tone1": {
+ "unicode": "270C-1F3FB",
+ "unicode_alternates": "",
+ "name": "victory hand tone 1",
+ "shortname": ":v_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "ohyeah",
+ "peace",
+ "two",
+ "v"
+ ]
+ },
+ "v_tone2": {
+ "unicode": "270C-1F3FC",
+ "unicode_alternates": "",
+ "name": "victory hand tone 2",
+ "shortname": ":v_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "ohyeah",
+ "peace",
+ "two",
+ "v"
+ ]
+ },
+ "v_tone3": {
+ "unicode": "270C-1F3FD",
+ "unicode_alternates": "",
+ "name": "victory hand tone 3",
+ "shortname": ":v_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "ohyeah",
+ "peace",
+ "two",
+ "v"
+ ]
+ },
+ "v_tone4": {
+ "unicode": "270C-1F3FE",
+ "unicode_alternates": "",
+ "name": "victory hand tone 4",
+ "shortname": ":v_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "ohyeah",
+ "peace",
+ "two",
+ "v"
+ ]
+ },
+ "v_tone5": {
+ "unicode": "270C-1F3FF",
+ "unicode_alternates": "",
+ "name": "victory hand tone 5",
+ "shortname": ":v_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "fingers",
+ "ohyeah",
+ "peace",
+ "two",
+ "v"
+ ]
+ },
"vertical_traffic_light": {
"unicode": "1F6A6",
"unicode_alternates": [],
@@ -12780,7 +28314,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["transportation", "traffic", "light", "stop", "go", "yield", "vertical"],
+ "keywords": [
+ "transportation",
+ "traffic",
+ "light",
+ "stop",
+ "go",
+ "yield",
+ "vertical"
+ ],
"moji": "🚦"
},
"vhs": {
@@ -12791,7 +28333,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["oldschool", "record", "video"],
+ "keywords": [
+ "oldschool",
+ "record",
+ "video"
+ ],
"moji": "📼"
},
"vibration_mode": {
@@ -12802,7 +28348,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["orange-square", "phone"],
+ "keywords": [
+ "orange-square",
+ "phone"
+ ],
"moji": "📳"
},
"video_camera": {
@@ -12813,7 +28362,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["film", "record"],
+ "keywords": [
+ "film",
+ "record"
+ ],
"moji": "📹"
},
"video_game": {
@@ -12824,7 +28376,19 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["PS4", "console", "controller", "play", "video", "game", "console", "controller", "nintendo", "xbox", "playstation"],
+ "keywords": [
+ "PS4",
+ "console",
+ "controller",
+ "play",
+ "video",
+ "game",
+ "console",
+ "controller",
+ "nintendo",
+ "xbox",
+ "playstation"
+ ],
"moji": "🎮"
},
"violin": {
@@ -12835,18 +28399,39 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["instrument", "music", "violin", "fiddle", "music", "instrument"],
+ "keywords": [
+ "instrument",
+ "music",
+ "violin",
+ "fiddle",
+ "music",
+ "instrument"
+ ],
"moji": "🎻"
},
"virgo": {
"unicode": "264D",
- "unicode_alternates": ["264D-FE0F"],
+ "unicode_alternates": [
+ "264D-FE0F"
+ ],
"name": "virgo",
"shortname": ":virgo:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sign", "virgo", "maiden", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "zodiac", "horoscope"],
+ "keywords": [
+ "sign",
+ "virgo",
+ "maiden",
+ "astrology",
+ "greek",
+ "constellation",
+ "stars",
+ "zodiac",
+ "sign",
+ "zodiac",
+ "horoscope"
+ ],
"moji": "♍"
},
"volcano": {
@@ -12857,9 +28442,27 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "photo", "volcano", "lava", "magma", "hot", "explode"],
+ "keywords": [
+ "nature",
+ "photo",
+ "volcano",
+ "lava",
+ "magma",
+ "hot",
+ "explode"
+ ],
"moji": "🌋"
},
+ "volleyball": {
+ "unicode": "1F3D0",
+ "unicode_alternates": "",
+ "name": "volleyball",
+ "shortname": ":volleyball:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": []
+ },
"vs": {
"unicode": "1F19A",
"unicode_alternates": [],
@@ -12868,7 +28471,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["orange-square", "words"],
+ "keywords": [
+ "orange-square",
+ "words"
+ ],
"moji": "🆚"
},
"vulcan": {
@@ -12877,9 +28483,113 @@
"name": "raised hand with part between middle and ring fingers",
"shortname": ":vulcan:",
"category": "people",
- "aliases": [":raised_hand_with_part_between_middle_and_ring_fingers:"],
- "aliases_ascii": [],
- "keywords": ["vulcan", "spock", "leonard", "nimoy", "star trek", "live long"]
+ "aliases": [
+ ":raised_hand_with_part_between_middle_and_ring_fingers:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "vulcan",
+ "spock",
+ "leonard",
+ "nimoy",
+ "star trek",
+ "live long"
+ ]
+ },
+ "vulcan_tone1": {
+ "unicode": "1F596-1F3FB",
+ "unicode_alternates": "",
+ "name": "raised hand with part between middle and ring fingers tone 1",
+ "shortname": ":vulcan_tone1:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_part_between_middle_and_ring_fingers_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "vulcan",
+ "spock",
+ "leonard",
+ "nimoy",
+ "star trek",
+ "live long"
+ ]
+ },
+ "vulcan_tone2": {
+ "unicode": "1F596-1F3FC",
+ "unicode_alternates": "",
+ "name": "raised hand with part between middle and ring fingers tone 2",
+ "shortname": ":vulcan_tone2:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_part_between_middle_and_ring_fingers_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "vulcan",
+ "spock",
+ "leonard",
+ "nimoy",
+ "star trek",
+ "live long"
+ ]
+ },
+ "vulcan_tone3": {
+ "unicode": "1F596-1F3FD",
+ "unicode_alternates": "",
+ "name": "raised hand with part between middle and ring fingers tone 3",
+ "shortname": ":vulcan_tone3:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_part_between_middle_and_ring_fingers_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "vulcan",
+ "spock",
+ "leonard",
+ "nimoy",
+ "star trek",
+ "live long"
+ ]
+ },
+ "vulcan_tone4": {
+ "unicode": "1F596-1F3FE",
+ "unicode_alternates": "",
+ "name": "raised hand with part between middle and ring fingers tone 4",
+ "shortname": ":vulcan_tone4:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_part_between_middle_and_ring_fingers_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "vulcan",
+ "spock",
+ "leonard",
+ "nimoy",
+ "star trek",
+ "live long"
+ ]
+ },
+ "vulcan_tone5": {
+ "unicode": "1F596-1F3FF",
+ "unicode_alternates": "",
+ "name": "raised hand with part between middle and ring fingers tone 5",
+ "shortname": ":vulcan_tone5:",
+ "category": "people",
+ "aliases": [
+ ":raised_hand_with_part_between_middle_and_ring_fingers_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "vulcan",
+ "spock",
+ "leonard",
+ "nimoy",
+ "star trek",
+ "live long"
+ ]
},
"walking": {
"unicode": "1F6B6",
@@ -12889,9 +28599,103 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["human", "man", "walk", "pedestrian", "stroll", "stride", "foot", "feet"],
+ "keywords": [
+ "human",
+ "man",
+ "walk",
+ "pedestrian",
+ "stroll",
+ "stride",
+ "foot",
+ "feet"
+ ],
"moji": "🚶"
},
+ "walking_tone1": {
+ "unicode": "1F6B6-1F3FB",
+ "unicode_alternates": "",
+ "name": "pedestrian tone 1",
+ "shortname": ":walking_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "man",
+ "walk",
+ "stroll",
+ "stride",
+ "hiking",
+ "hike"
+ ]
+ },
+ "walking_tone2": {
+ "unicode": "1F6B6-1F3FC",
+ "unicode_alternates": "",
+ "name": "pedestrian tone 2",
+ "shortname": ":walking_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "man",
+ "walk",
+ "stroll",
+ "stride",
+ "hiking",
+ "hike"
+ ]
+ },
+ "walking_tone3": {
+ "unicode": "1F6B6-1F3FD",
+ "unicode_alternates": "",
+ "name": "pedestrian tone 3",
+ "shortname": ":walking_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "man",
+ "walk",
+ "stroll",
+ "stride",
+ "hiking",
+ "hike"
+ ]
+ },
+ "walking_tone4": {
+ "unicode": "1F6B6-1F3FE",
+ "unicode_alternates": "",
+ "name": "pedestrian tone 4",
+ "shortname": ":walking_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "man",
+ "walk",
+ "stroll",
+ "stride",
+ "hiking",
+ "hike"
+ ]
+ },
+ "walking_tone5": {
+ "unicode": "1F6B6-1F3FF",
+ "unicode_alternates": "",
+ "name": "pedestrian tone 5",
+ "shortname": ":walking_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "man",
+ "walk",
+ "stroll",
+ "stride",
+ "hiking",
+ "hike"
+ ]
+ },
"waning_crescent_moon": {
"unicode": "1F318",
"unicode_alternates": [],
@@ -12900,7 +28704,16 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "crescent", "waning", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "crescent",
+ "waning",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌘"
},
"waning_gibbous_moon": {
@@ -12911,18 +28724,32 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "waning", "gibbous", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "waning",
+ "gibbous",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌖"
},
"warning": {
"unicode": "26A0",
- "unicode_alternates": ["26A0-FE0F"],
+ "unicode_alternates": [
+ "26A0-FE0F"
+ ],
"name": "warning sign",
"shortname": ":warning:",
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["exclamation", "wip"],
+ "keywords": [
+ "exclamation",
+ "wip"
+ ],
"moji": "⚠"
},
"wastebasket": {
@@ -12933,17 +28760,26 @@
"category": "objects_symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["trash", "garbage", "dispose"]
+ "keywords": [
+ "trash",
+ "garbage",
+ "dispose"
+ ]
},
"watch": {
"unicode": "231A",
- "unicode_alternates": ["231A-FE0F"],
+ "unicode_alternates": [
+ "231A-FE0F"
+ ],
"name": "watch",
"shortname": ":watch:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["accessories", "time"],
+ "keywords": [
+ "accessories",
+ "time"
+ ],
"moji": "⌚"
},
"water_buffalo": {
@@ -12954,7 +28790,18 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "cow", "nature", "ox", "water", "buffalo", "asia", "bovine", "milk", "dairy"],
+ "keywords": [
+ "animal",
+ "cow",
+ "nature",
+ "ox",
+ "water",
+ "buffalo",
+ "asia",
+ "bovine",
+ "milk",
+ "dairy"
+ ],
"moji": "🐃"
},
"watermelon": {
@@ -12965,7 +28812,15 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["food", "fruit", "melon", "watermelon", "summer", "fruit", "large"],
+ "keywords": [
+ "food",
+ "fruit",
+ "melon",
+ "watermelon",
+ "summer",
+ "fruit",
+ "large"
+ ],
"moji": "🍉"
},
"wave": {
@@ -12976,9 +28831,100 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["farewell", "gesture", "goodbye", "hands", "solong"],
+ "keywords": [
+ "farewell",
+ "gesture",
+ "goodbye",
+ "hands",
+ "solong"
+ ],
"moji": "👋"
},
+ "wave_tone1": {
+ "unicode": "1F44B-1F3FB",
+ "unicode_alternates": "",
+ "name": "waving hand sign tone 1",
+ "shortname": ":wave_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "farewell",
+ "gesture",
+ "goodbye",
+ "solong",
+ "hi",
+ "wave"
+ ]
+ },
+ "wave_tone2": {
+ "unicode": "1F44B-1F3FC",
+ "unicode_alternates": "",
+ "name": "waving hand sign tone 2",
+ "shortname": ":wave_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "farewell",
+ "gesture",
+ "goodbye",
+ "solong",
+ "hi",
+ "wave"
+ ]
+ },
+ "wave_tone3": {
+ "unicode": "1F44B-1F3FD",
+ "unicode_alternates": "",
+ "name": "waving hand sign tone 3",
+ "shortname": ":wave_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "farewell",
+ "gesture",
+ "goodbye",
+ "solong",
+ "hi",
+ "wave"
+ ]
+ },
+ "wave_tone4": {
+ "unicode": "1F44B-1F3FE",
+ "unicode_alternates": "",
+ "name": "waving hand sign tone 4",
+ "shortname": ":wave_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "farewell",
+ "gesture",
+ "goodbye",
+ "solong",
+ "hi",
+ "wave"
+ ]
+ },
+ "wave_tone5": {
+ "unicode": "1F44B-1F3FF",
+ "unicode_alternates": "",
+ "name": "waving hand sign tone 5",
+ "shortname": ":wave_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "farewell",
+ "gesture",
+ "goodbye",
+ "solong",
+ "hi",
+ "wave"
+ ]
+ },
"wavy_dash": {
"unicode": "3030",
"unicode_alternates": [],
@@ -12987,7 +28933,10 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["draw", "line"],
+ "keywords": [
+ "draw",
+ "line"
+ ],
"moji": "〰"
},
"waxing_crescent_moon": {
@@ -12998,7 +28947,15 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature", "moon", "waxing", "sky", "night", "cheese", "phase"],
+ "keywords": [
+ "nature",
+ "moon",
+ "waxing",
+ "sky",
+ "night",
+ "cheese",
+ "phase"
+ ],
"moji": "🌒"
},
"waxing_gibbous_moon": {
@@ -13009,7 +28966,9 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["nature"],
+ "keywords": [
+ "nature"
+ ],
"moji": "🌔"
},
"wc": {
@@ -13020,7 +28979,20 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "restroom", "toilet", "water", "closet", "toilet", "bathroom", "throne", "porcelain", "waste", "flush", "plumbing"],
+ "keywords": [
+ "blue-square",
+ "restroom",
+ "toilet",
+ "water",
+ "closet",
+ "toilet",
+ "bathroom",
+ "throne",
+ "porcelain",
+ "waste",
+ "flush",
+ "plumbing"
+ ],
"moji": "🚾"
},
"weary": {
@@ -13031,7 +29003,21 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "frustrated", "sad", "sleepy", "tired", "weary", "sleepy", "tired", "tiredness", "study", "finals", "school", "exhausted"],
+ "keywords": [
+ "face",
+ "frustrated",
+ "sad",
+ "sleepy",
+ "tired",
+ "weary",
+ "sleepy",
+ "tired",
+ "tiredness",
+ "study",
+ "finals",
+ "school",
+ "exhausted"
+ ],
"moji": "😩"
},
"wedding": {
@@ -13042,7 +29028,15 @@
"category": "places",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "bride", "couple", "groom", "like", "love", "marriage"],
+ "keywords": [
+ "affection",
+ "bride",
+ "couple",
+ "groom",
+ "like",
+ "love",
+ "marriage"
+ ],
"moji": "💒"
},
"whale": {
@@ -13053,7 +29047,12 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "ocean", "sea"],
+ "keywords": [
+ "animal",
+ "nature",
+ "ocean",
+ "sea"
+ ],
"moji": "🐳"
},
"whale2": {
@@ -13064,18 +29063,48 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature", "ocean", "sea", "whale", "blubber", "bloated", "fat", "large", "massive"],
+ "keywords": [
+ "animal",
+ "nature",
+ "ocean",
+ "sea",
+ "whale",
+ "blubber",
+ "bloated",
+ "fat",
+ "large",
+ "massive"
+ ],
"moji": "🐋"
},
+ "wheel_of_dharma": {
+ "unicode": "2638",
+ "unicode_alternates": "",
+ "name": "wheel of dharma",
+ "shortname": ":wheel_of_dharma:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "buddhist",
+ "religion",
+ "symbol"
+ ]
+ },
"wheelchair": {
"unicode": "267F",
- "unicode_alternates": ["267F-FE0F"],
+ "unicode_alternates": [
+ "267F-FE0F"
+ ],
"name": "wheelchair symbol",
"shortname": ":wheelchair:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "disabled"],
+ "keywords": [
+ "blue-square",
+ "disabled"
+ ],
"moji": "♿"
},
"white_check_mark": {
@@ -13086,18 +29115,26 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["agree", "green-square", "ok"],
+ "keywords": [
+ "agree",
+ "green-square",
+ "ok"
+ ],
"moji": "✅"
},
"white_circle": {
"unicode": "26AA",
- "unicode_alternates": ["26AA-FE0F"],
+ "unicode_alternates": [
+ "26AA-FE0F"
+ ],
"name": "medium white circle",
"shortname": ":white_circle:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "⚪"
},
"white_flower": {
@@ -13108,51 +29145,81 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["japanese", "white", "flower", "teacher", "school", "grade", "score", "brilliance", "intelligence", "homework", "student", "assignment", "praise"],
+ "keywords": [
+ "japanese",
+ "white",
+ "flower",
+ "teacher",
+ "school",
+ "grade",
+ "score",
+ "brilliance",
+ "intelligence",
+ "homework",
+ "student",
+ "assignment",
+ "praise"
+ ],
"moji": "💮"
},
"white_large_square": {
"unicode": "2B1C",
- "unicode_alternates": ["2B1C-FE0F"],
+ "unicode_alternates": [
+ "2B1C-FE0F"
+ ],
"name": "white large square",
"shortname": ":white_large_square:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "⬜"
},
"white_medium_small_square": {
"unicode": "25FD",
- "unicode_alternates": ["25FD-FE0F"],
+ "unicode_alternates": [
+ "25FD-FE0F"
+ ],
"name": "white medium small square",
"shortname": ":white_medium_small_square:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "◽"
},
"white_medium_square": {
"unicode": "25FB",
- "unicode_alternates": ["25FB-FE0F"],
+ "unicode_alternates": [
+ "25FB-FE0F"
+ ],
"name": "white medium square",
"shortname": ":white_medium_square:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "◻"
},
"white_small_square": {
"unicode": "25AB",
- "unicode_alternates": ["25AB-FE0F"],
+ "unicode_alternates": [
+ "25AB-FE0F"
+ ],
"name": "white small square",
"shortname": ":white_small_square:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "▫"
},
"white_square_button": {
@@ -13163,9 +29230,56 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["shape"],
+ "keywords": [
+ "shape"
+ ],
"moji": "🔳"
},
+ "white_sun_cloud": {
+ "unicode": "1F325",
+ "unicode_alternates": "",
+ "name": "white sun behind cloud",
+ "shortname": ":white_sun_cloud:",
+ "category": "nature",
+ "aliases": [
+ ":white_sun_behind_cloud:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "nature",
+ "weather"
+ ]
+ },
+ "white_sun_rain_cloud": {
+ "unicode": "1F326",
+ "unicode_alternates": "",
+ "name": "white sun behind cloud with rain",
+ "shortname": ":white_sun_rain_cloud:",
+ "category": "nature",
+ "aliases": [
+ ":white_sun_behind_cloud_with_rain:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "nature",
+ "weather"
+ ]
+ },
+ "white_sun_small_cloud": {
+ "unicode": "1F324",
+ "unicode_alternates": "",
+ "name": "white sun with small cloud",
+ "shortname": ":white_sun_small_cloud:",
+ "category": "nature",
+ "aliases": [
+ ":white_sun_with_small_cloud:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "nature",
+ "weather"
+ ]
+ },
"wind_blowing_face": {
"unicode": "1F32C",
"unicode_alternates": [],
@@ -13174,7 +29288,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["mother", "nature"]
+ "keywords": [
+ "mother",
+ "nature"
+ ]
},
"wind_chime": {
"unicode": "1F390",
@@ -13184,7 +29301,21 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["ding", "nature", "wind", "chime", "bell", "fūrin", "instrument", "music", "spirits", "soothing", "protective", "spiritual", "sound"],
+ "keywords": [
+ "ding",
+ "nature",
+ "wind",
+ "chime",
+ "bell",
+ "fūrin",
+ "instrument",
+ "music",
+ "spirits",
+ "soothing",
+ "protective",
+ "spiritual",
+ "sound"
+ ],
"moji": "🎐"
},
"wine_glass": {
@@ -13195,7 +29326,20 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["alcohol", "beverage", "booze", "bottle", "drink", "drunk", "fermented", "glass", "grapes", "tasting", "wine", "winery"],
+ "keywords": [
+ "alcohol",
+ "beverage",
+ "booze",
+ "bottle",
+ "drink",
+ "drunk",
+ "fermented",
+ "glass",
+ "grapes",
+ "tasting",
+ "wine",
+ "winery"
+ ],
"moji": "🍷"
},
"wink": {
@@ -13205,8 +29349,26 @@
"shortname": ":wink:",
"category": "emoticons",
"aliases": [],
- "aliases_ascii": [";)", ";-)", "*-)", "*)", ";-]", ";]", ";D", ";^)"],
- "keywords": ["face", "happy", "mischievous", "secret", "wink", "winking", "friendly", "joke"],
+ "aliases_ascii": [
+ ";)",
+ ";-)",
+ "*-)",
+ "*)",
+ ";-]",
+ ";]",
+ ";D",
+ ";^)"
+ ],
+ "keywords": [
+ "face",
+ "happy",
+ "mischievous",
+ "secret",
+ "wink",
+ "winking",
+ "friendly",
+ "joke"
+ ],
"moji": "😉"
},
"wolf": {
@@ -13217,7 +29379,10 @@
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["animal", "nature"],
+ "keywords": [
+ "animal",
+ "nature"
+ ],
"moji": "🐺"
},
"woman": {
@@ -13228,9 +29393,82 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["female", "girls"],
+ "keywords": [
+ "female",
+ "girls"
+ ],
"moji": "👩"
},
+ "woman_tone1": {
+ "unicode": "1F469-1F3FB",
+ "unicode_alternates": "",
+ "name": "woman tone 1",
+ "shortname": ":woman_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "lady"
+ ]
+ },
+ "woman_tone2": {
+ "unicode": "1F469-1F3FC",
+ "unicode_alternates": "",
+ "name": "woman tone 2",
+ "shortname": ":woman_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "lady"
+ ]
+ },
+ "woman_tone3": {
+ "unicode": "1F469-1F3FD",
+ "unicode_alternates": "",
+ "name": "woman tone 3",
+ "shortname": ":woman_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "lady"
+ ]
+ },
+ "woman_tone4": {
+ "unicode": "1F469-1F3FE",
+ "unicode_alternates": "",
+ "name": "woman tone 4",
+ "shortname": ":woman_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "lady"
+ ]
+ },
+ "woman_tone5": {
+ "unicode": "1F469-1F3FF",
+ "unicode_alternates": "",
+ "name": "woman tone 5",
+ "shortname": ":woman_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "female",
+ "girl",
+ "lady"
+ ]
+ },
"womans_clothes": {
"unicode": "1F45A",
"unicode_alternates": [],
@@ -13239,7 +29477,21 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["fashion", "woman", "clothing", "clothes", "blouse", "shirt", "wardrobe", "breasts", "cleavage", "shopping", "shop", "dressing", "dressed"],
+ "keywords": [
+ "fashion",
+ "woman",
+ "clothing",
+ "clothes",
+ "blouse",
+ "shirt",
+ "wardrobe",
+ "breasts",
+ "cleavage",
+ "shopping",
+ "shop",
+ "dressing",
+ "dressed"
+ ],
"moji": "👚"
},
"womans_hat": {
@@ -13250,7 +29502,11 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["accessories", "fashion", "female"],
+ "keywords": [
+ "accessories",
+ "fashion",
+ "female"
+ ],
"moji": "👒"
},
"womens": {
@@ -13261,7 +29517,16 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["purple-square", "woman", "bathroom", "restroom", "sign", "girl", "female", "avatar"],
+ "keywords": [
+ "purple-square",
+ "woman",
+ "bathroom",
+ "restroom",
+ "sign",
+ "girl",
+ "female",
+ "avatar"
+ ],
"moji": "🚺"
},
"worried": {
@@ -13272,7 +29537,16 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["concern", "face", "nervous", "worried", "anxious", "distressed", "nervous", "tense"],
+ "keywords": [
+ "concern",
+ "face",
+ "nervous",
+ "worried",
+ "anxious",
+ "distressed",
+ "nervous",
+ "tense"
+ ],
"moji": "😟"
},
"wrench": {
@@ -13283,7 +29557,11 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["diy", "ikea", "tools"],
+ "keywords": [
+ "diy",
+ "ikea",
+ "tools"
+ ],
"moji": "🔧"
},
"writing_hand": {
@@ -13292,9 +29570,91 @@
"name": "left writing hand",
"shortname": ":writing_hand:",
"category": "people",
- "aliases": [":left_writing_hand:"],
+ "aliases": [
+ ":left_writing_hand:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "write",
+ "sign",
+ "signature",
+ "draw"
+ ]
+ },
+ "writing_hand_tone1": {
+ "unicode": "270D-1F3FB",
+ "unicode_alternates": "",
+ "name": "writing hand tone 1",
+ "shortname": ":writing_hand_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "write",
+ "sign",
+ "signature",
+ "draw"
+ ]
+ },
+ "writing_hand_tone2": {
+ "unicode": "270D-1F3FC",
+ "unicode_alternates": "",
+ "name": "writing hand tone 2",
+ "shortname": ":writing_hand_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "write",
+ "sign",
+ "signature",
+ "draw"
+ ]
+ },
+ "writing_hand_tone3": {
+ "unicode": "270D-1F3FD",
+ "unicode_alternates": "",
+ "name": "writing hand tone 3",
+ "shortname": ":writing_hand_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "write",
+ "sign",
+ "signature",
+ "draw"
+ ]
+ },
+ "writing_hand_tone4": {
+ "unicode": "270D-1F3FE",
+ "unicode_alternates": "",
+ "name": "writing hand tone 4",
+ "shortname": ":writing_hand_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "write",
+ "sign",
+ "signature",
+ "draw"
+ ]
+ },
+ "writing_hand_tone5": {
+ "unicode": "270D-1F3FF",
+ "unicode_alternates": "",
+ "name": "writing hand tone 5",
+ "shortname": ":writing_hand_tone5:",
+ "category": "people",
+ "aliases": [],
"aliases_ascii": [],
- "keywords": ["write", "sign", "signature", "draw"]
+ "keywords": [
+ "write",
+ "sign",
+ "signature",
+ "draw"
+ ]
},
"x": {
"unicode": "274C",
@@ -13304,7 +29664,11 @@
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["delete", "no", "remove"],
+ "keywords": [
+ "delete",
+ "no",
+ "remove"
+ ],
"moji": "❌"
},
"yellow_heart": {
@@ -13315,7 +29679,25 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["affection", "like", "love", "valentines", "yellow", "gold", "heart", "love", "friendship", "happy", "happiness", "trust", "compassionate", "respectful", "honest", "caring", "selfless"],
+ "keywords": [
+ "affection",
+ "like",
+ "love",
+ "valentines",
+ "yellow",
+ "gold",
+ "heart",
+ "love",
+ "friendship",
+ "happy",
+ "happiness",
+ "trust",
+ "compassionate",
+ "respectful",
+ "honest",
+ "caring",
+ "selfless"
+ ],
"moji": "💛"
},
"yen": {
@@ -13326,9 +29708,39 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["currency", "dollar", "japanese", "money", "yen", "japan", "japanese", "banknote", "money", "currency", "paper", "cash", "bill"],
+ "keywords": [
+ "currency",
+ "dollar",
+ "japanese",
+ "money",
+ "yen",
+ "japan",
+ "japanese",
+ "banknote",
+ "money",
+ "currency",
+ "paper",
+ "cash",
+ "bill"
+ ],
"moji": "💴"
},
+ "yin_yang": {
+ "unicode": "262F",
+ "unicode_alternates": "",
+ "name": "yin yang",
+ "shortname": ":yin_yang:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "religion",
+ "sign",
+ "symbol",
+ "tao",
+ "taoist"
+ ]
+ },
"yum": {
"unicode": "1F60B",
"unicode_alternates": [],
@@ -13337,30 +29749,68 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["face", "happy", "joy", "smile", "tongue", "delicious", "savoring", "food", "eat", "yummy", "yum", "tasty", "savory"],
+ "keywords": [
+ "face",
+ "happy",
+ "joy",
+ "smile",
+ "tongue",
+ "delicious",
+ "savoring",
+ "food",
+ "eat",
+ "yummy",
+ "yum",
+ "tasty",
+ "savory"
+ ],
"moji": "😋"
},
"zap": {
"unicode": "26A1",
- "unicode_alternates": ["26A1-FE0F"],
+ "unicode_alternates": [
+ "26A1-FE0F"
+ ],
"name": "high voltage sign",
"shortname": ":zap:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["lightning bolt", "thunder", "weather"],
+ "keywords": [
+ "lightning bolt",
+ "thunder",
+ "weather"
+ ],
"moji": "⚡"
},
"zero": {
"moji": "0️⃣",
"unicode": "0030-20E3",
- "unicode_alternates": ["0030-FE0F-20E3"],
+ "unicode_alternates": [
+ "0030-FE0F-20E3"
+ ],
"name": "digit zero",
"shortname": ":zero:",
"category": "other",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["blue-square", "null", "numbers"]
+ "keywords": [
+ "blue-square",
+ "null",
+ "numbers"
+ ]
+ },
+ "zipper_mouth": {
+ "unicode": "1F910",
+ "unicode_alternates": "",
+ "name": "zipper-mouth face",
+ "shortname": ":zipper_mouth:",
+ "category": "people",
+ "aliases": [
+ ":zipper_mouth_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": []
},
"zzz": {
"unicode": "1F4A4",
@@ -13370,7 +29820,10 @@
"category": "emoticons",
"aliases": [],
"aliases_ascii": [],
- "keywords": ["sleepy", "tired"],
+ "keywords": [
+ "sleepy",
+ "tired"
+ ],
"moji": "💤"
}
}
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7834262d612..7d65145176b 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -54,5 +54,8 @@ module API
mount Keys
mount Tags
mount Triggers
+ mount Builds
+ mount Variables
+ mount Runners
end
end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
new file mode 100644
index 00000000000..2b104f90aa7
--- /dev/null
+++ b/lib/api/builds.rb
@@ -0,0 +1,204 @@
+module API
+ # Projects builds API
+ class Builds < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Get a project builds
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
+ # if none provided showing all builds)
+ # Example Request:
+ # GET /projects/:id/builds
+ get ':id/builds' do
+
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ # Get builds for a specific commit of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # sha (required) - The SHA id of a commit
+ # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
+ # if none provided showing all builds)
+ # Example Request:
+ # GET /projects/:id/repository/commits/:sha/builds
+ get ':id/repository/commits/:sha/builds' do
+ authorize_read_builds!
+
+ commit = user_project.ci_commits.find_by_sha(params[:sha])
+ return not_found! unless commit
+
+ builds = commit.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ # Get a specific build of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # build_id (required) - The ID of a build
+ # Example Request:
+ # GET /projects/:id/builds/:build_id
+ get ':id/builds/:build_id' do
+ authorize_read_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ # Download the artifacts file from build
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # Example Request:
+ # GET /projects/:id/builds/:build_id/artifacts
+ get ':id/builds/:build_id/artifacts' do
+ authorize_read_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ artifacts_file = build.artifacts_file
+
+ unless artifacts_file.file_storage?
+ return redirect_to build.artifacts_file.url
+ end
+
+ return not_found! unless artifacts_file.exists?
+
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ end
+
+ # Get a trace of a specific build of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # build_id (required) - The ID of a build
+ # Example Request:
+ # GET /projects/:id/build/:build_id/trace
+ #
+ # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ get ':id/builds/:build_id/trace' do
+ authorize_read_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ trace = build.trace
+ body trace
+ end
+
+ # Cancel a specific build of a project
+ #
+ # parameters:
+ # id (required) - the id of a project
+ # build_id (required) - the id of a build
+ # example request:
+ # post /projects/:id/build/:build_id/cancel
+ post ':id/builds/:build_id/cancel' do
+ authorize_update_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ build.cancel
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ # Retry a specific build of a project
+ #
+ # parameters:
+ # id (required) - the id of a project
+ # build_id (required) - the id of a build
+ # example request:
+ # post /projects/:id/build/:build_id/retry
+ post ':id/builds/:build_id/retry' do
+ authorize_update_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+ return forbidden!('Build is not retryable') unless build.retryable?
+
+ build = Ci::Build.retry(build)
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ # Erase build (remove artifacts and build trace)
+ #
+ # Parameters:
+ # id (required) - the id of a project
+ # build_id (required) - the id of a build
+ # example Request:
+ # post /projects/:id/build/:build_id/erase
+ post ':id/builds/:build_id/erase' do
+ authorize_update_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+ return forbidden!('Build is not erasable!') unless build.erasable?
+
+ build.erase(erased_by: current_user)
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+ end
+
+ helpers do
+ def get_build(id)
+ user_project.builds.find_by(id: id.to_i)
+ end
+
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+ scope =
+ if scope.is_a?(String)
+ [scope]
+ elsif scope.is_a?(Hashie::Mash)
+ scope.values
+ else
+ ['unknown']
+ end
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+
+ def authorize_read_builds!
+ authorize! :read_build, user_project
+ end
+
+ def authorize_update_builds!
+ authorize! :update_build, user_project
+ end
+ end
+ end
+end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 1162271f5fc..8e74e177ea0 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -18,10 +18,12 @@ module API
# Examples:
# GET /projects/:id/repository/commits/:sha/statuses
get ':id/repository/commits/:sha/statuses' do
- authorize! :read_commit_statuses, user_project
- sha = params[:sha]
- ci_commit = user_project.ci_commit(sha)
- not_found! 'Commit' unless ci_commit
+ authorize!(:read_commit_status, user_project)
+
+ not_found!('Commit') unless user_project.commit(params[:sha])
+ ci_commit = user_project.ci_commit(params[:sha])
+ return [] unless ci_commit
+
statuses = ci_commit.statuses
statuses = statuses.latest unless parse_boolean(params[:all])
statuses = statuses.where(ref: params[:ref]) if params[:ref].present?
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index f4efb651eb6..4544a41b1e3 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -48,7 +48,7 @@ module API
sha = params[:sha]
commit = user_project.commit(sha)
not_found! "Commit" unless commit
- commit.diffs
+ commit.diffs.to_a
end
# Get a commit's comments
@@ -90,9 +90,9 @@ module API
}
if params[:path] && params[:line] && params[:line_type]
- commit.diffs.each do |diff|
+ commit.diffs(all_diffs: true).each do |diff|
next unless diff.new_path == params[:path]
- lines = Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a)
+ lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
lines.each do |line|
next unless line.new_pos == params[:line].to_i && line.type == params[:line_type]
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 26e7c956e8f..71197205f34 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -23,12 +23,15 @@ module API
end
class UserFull < User
+ expose :last_sign_in_at
+ expose :confirmed_at
expose :email
expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
expose :two_factor_enabled
+ expose :external
end
class UserLogin < UserFull
@@ -49,7 +52,7 @@ module API
expose :enable_ssl_verification
end
- class ForkedFromProject < Grape::Entity
+ class BasicProjectDetails < Grape::Entity
expose :id
expose :name, :name_with_namespace
expose :path, :path_with_namespace
@@ -67,10 +70,12 @@ module API
expose :shared_runners_enabled
expose :creator_id
expose :namespace
- expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ |project, options| project.forked? }
+ expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
expose :avatar_url
expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? }
+ expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
+ expose :public_builds
end
class ProjectMember < UserBasic
@@ -139,7 +144,10 @@ module API
class ProjectSnippet < Grape::Entity
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
- expose :expires_at, :updated_at, :created_at
+ expose :updated_at, :created_at
+
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ expose(:expires_at) { |snippet| nil }
end
class ProjectEntity < Grape::Entity
@@ -174,11 +182,12 @@ module API
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
expose :merge_when_build_succeeds
+ expose :merge_status
end
class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
- compare.diffs
+ compare.diffs(all_diffs: true).to_a
end
end
@@ -238,6 +247,10 @@ module API
end
end
+ class ProjectGroupLink < Grape::Entity
+ expose :id, :project_id, :group_id, :group_access
+ end
+
class Namespace < Grape::Entity
expose :id, :path, :kind
end
@@ -292,11 +305,11 @@ module API
end
expose :diffs, using: Entities::RepoDiff do |compare, options|
- compare.diffs
+ compare.diffs(all_diffs: true).to_a
end
expose :compare_timeout do |compare, options|
- compare.timeout
+ compare.diffs.overflow?
end
expose :same, as: :compare_same_ref
@@ -365,5 +378,52 @@ module API
class TriggerRequest < Grape::Entity
expose :id, :variables
end
+
+ class Runner < Grape::Entity
+ expose :id
+ expose :description
+ expose :active
+ expose :is_shared
+ expose :name
+ end
+
+ class RunnerDetails < Runner
+ expose :tag_list
+ expose :version, :revision, :platform, :architecture
+ expose :contacted_at
+ expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? }
+ expose :projects, with: Entities::BasicProjectDetails do |runner, options|
+ if options[:current_user].is_admin?
+ runner.projects
+ else
+ options[:current_user].authorized_projects.where(id: runner.projects)
+ end
+ end
+ end
+
+ class BuildArtifactFile < Grape::Entity
+ expose :filename, :size
+ end
+
+ class Build < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage
+ expose :created_at, :started_at, :finished_at
+ expose :user, with: User
+ expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :commit, with: RepoCommit do |repo_obj, _options|
+ if repo_obj.respond_to?(:commit)
+ repo_obj.commit.commit_data
+ end
+ end
+ expose :runner, with: Runner
+ end
+
+ class Trigger < Grape::Entity
+ expose :token, :created_at, :updated_at, :deleted_at, :last_used
+ end
+
+ class Variable < Grape::Entity
+ expose :key, :value
+ end
end
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 8ad2c1883c7..c1d86f313b0 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -58,9 +58,11 @@ module API
commit = user_project.commit(ref)
not_found! 'Commit' unless commit
- blob = user_project.repository.blob_at(commit.sha, file_path)
+ repo = user_project.repository
+ blob = repo.blob_at(commit.sha, file_path)
if blob
+ blob.load_all_data!(repo)
status(200)
{
@@ -72,7 +74,7 @@ module API
ref: ref,
blob_id: blob.id,
commit_id: commit.id,
- last_commit_id: user_project.repository.last_commit_for_path(commit.sha, file_path).id
+ last_commit_id: repo.last_commit_for_path(commit.sha, file_path).id
}
else
not_found! 'File'
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a4df810e755..a72044e8058 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -30,7 +30,7 @@ module API
end
def sudo_identifier()
- identifier ||= params[SUDO_PARAM] ||= env[SUDO_HEADER]
+ identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
# Regex for integers
if !!(identifier =~ /^[0-9]+$/)
@@ -97,11 +97,9 @@ module API
end
def paginate(relation)
- per_page = params[:per_page].to_i
- paginated = relation.page(params[:page]).per(per_page)
- add_pagination_headers(paginated, per_page)
-
- paginated
+ relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ add_pagination_headers(data)
+ end
end
def authenticate!
@@ -155,10 +153,11 @@ module API
end
def attributes_for_keys(keys, custom_params = nil)
+ params_hash = custom_params || params
attrs = {}
keys.each do |key|
- if params[key].present? or (params.has_key?(key) and params[key] == false)
- attrs[key] = params[key]
+ if params_hash[key].present? or (params_hash.has_key?(key) and params_hash[key] == false)
+ attrs[key] = params_hash[key]
end
end
ActionController::Parameters.new(attrs).permit!
@@ -266,6 +265,10 @@ module API
projects = projects.search(params[:search])
end
+ if params[:visibility].present?
+ projects = projects.search_by_visibility(params[:visibility])
+ end
+
projects.reorder(project_order_by => project_sort)
end
@@ -289,12 +292,14 @@ module API
# file helpers
- def uploaded_file!(field, uploads_path)
+ def uploaded_file(field, uploads_path)
if params[field]
bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
return params[field]
end
+ return nil unless params["#{field}.path"] && params["#{field}.name"]
+
# sanitize file paths
# this requires all paths to exist
required_attributes! %W(#{field}.path)
@@ -327,16 +332,36 @@ module API
private
- def add_pagination_headers(paginated, per_page)
+ def add_pagination_headers(paginated_data)
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', paginated_data.total_pages.to_s
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+ end
+
+ def pagination_links(paginated_data)
request_url = request.url.split('?').first
+ request_params = params.clone
+ request_params[:per_page] = paginated_data.limit_value
links = []
- links << %(<#{request_url}?page=#{paginated.current_page - 1}&per_page=#{per_page}>; rel="prev") unless paginated.first_page?
- links << %(<#{request_url}?page=#{paginated.current_page + 1}&per_page=#{per_page}>; rel="next") unless paginated.last_page?
- links << %(<#{request_url}?page=1&per_page=#{per_page}>; rel="first")
- links << %(<#{request_url}?page=#{paginated.total_pages}&per_page=#{per_page}>; rel="last")
- header 'Link', links.join(', ')
+ request_params[:page] = paginated_data.current_page - 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
+
+ request_params[:page] = paginated_data.current_page + 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
+
+ request_params[:page] = 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
+
+ request_params[:page] = paginated_data.total_pages
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
+
+ links.join(', ')
end
def abilities
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index e38736fc28b..2200208b946 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -14,6 +14,14 @@ module API
# ref - branch name
# forced_push - forced_push
#
+
+ helpers do
+ def wiki?
+ @wiki ||= params[:project].end_with?('.wiki') &&
+ !Project.find_with_namespace(params[:project])
+ end
+ end
+
post "/allowed" do
status 200
@@ -30,13 +38,12 @@ module API
# Strip out the .wiki from the pathname before finding the
# project. This applies the correct project permissions to
# the wiki repository as well.
- wiki = project_path.end_with?('.wiki')
- project_path.chomp!('.wiki') if wiki
+ project_path.chomp!('.wiki') if wiki?
project = Project.find_with_namespace(project_path)
access =
- if wiki
+ if wiki?
Gitlab::GitAccessWiki.new(actor, project)
else
Gitlab::GitAccess.new(actor, project)
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 6e7a7672070..fda6f841438 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -3,6 +3,8 @@ module API
class Issues < Grape::API
before { authenticate! }
+ helpers ::Gitlab::AkismetHelper
+
helpers do
def filter_issues_state(issues, state)
case state
@@ -19,6 +21,17 @@ module API
def filter_issues_milestone(issues, milestone)
issues.includes(:milestone).where('milestones.title' => milestone)
end
+
+ def create_spam_log(project, current_user, attrs)
+ params = attrs.merge({
+ source_ip: env['REMOTE_ADDR'],
+ user_agent: env['HTTP_USER_AGENT'],
+ noteable_type: 'Issue',
+ via_api: true
+ })
+
+ ::CreateSpamLogService.new(project, current_user, params).execute
+ end
end
resource :issues do
@@ -69,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?
@@ -91,6 +104,7 @@ 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
@@ -114,7 +128,15 @@ module API
render_api_error!({ labels: errors }, 400)
end
- issue = ::Issues::CreateService.new(user_project, current_user, attrs).execute
+ project = user_project
+ text = [attrs[:title], attrs[:description]].reject(&:blank?).join("\n")
+
+ if check_for_spam?(project, current_user) && is_spam?(env, current_user, text)
+ create_spam_log(project, current_user, attrs)
+ render_api_error!({ error: 'Spam detected' }, 400)
+ end
+
+ issue = ::Issues::CreateService.new(project, current_user, attrs).execute
if issue.valid?
# Find or create labels and attach to issue. Labels are valid because
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 3c1c6bda260..c5e5d57ed4d 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -59,55 +59,6 @@ module API
present paginate(merge_requests), with: Entities::MergeRequest
end
- # Show MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_request/:merge_request_id
- #
- get ":id/merge_request/:merge_request_id" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
- authorize! :read_merge_request, merge_request
-
- present merge_request, with: Entities::MergeRequest
- end
-
- # Show MR commits
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_request/:merge_request_id/commits
- #
- get ':id/merge_request/:merge_request_id/commits' do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
- present merge_request.commits, with: Entities::RepoCommit
- end
-
- # Show MR changes
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_request/:merge_request_id/changes
- #
- get ':id/merge_request/:merge_request_id/changes' do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
- present merge_request, with: Entities::MergeRequestChanges
- end
-
# Create MR
#
# Parameters:
@@ -120,6 +71,7 @@ module API
# title (required) - Title of MR
# description - Description of MR
# labels (optional) - Labels for MR as a comma-separated list
+ # milestone_id (optional) - Milestone ID
#
# Example:
# POST /projects/:id/merge_requests
@@ -127,7 +79,7 @@ module API
post ":id/merge_requests" do
authorize! :create_merge_request, user_project
required_attributes! [:source_branch, :target_branch, :title]
- attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description]
+ attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id]
# Validate label names in advance
if (errors = validate_label_params(params)).any?
@@ -148,146 +100,220 @@ module API
end
end
- # Update MR
+ # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
+ # Use "merge_requests/:merge_request_id/..." instead.
#
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # target_branch - The target branch
- # assignee_id - Assignee user ID
- # title - Title of MR
- # state_event - Status of MR. (close|reopen|merge)
- # description - Description of MR
- # labels (optional) - Labels for a MR as a comma-separated list
- # Example:
- # PUT /projects/:id/merge_request/:merge_request_id
- #
- put ":id/merge_request/:merge_request_id" do
- attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description]
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :update_merge_request, merge_request
-
- # Ensure source_branch is not specified
- if params[:source_branch].present?
- render_api_error!('Source branch cannot be changed', 400)
- end
-
- # Validate label names in advance
- if (errors = validate_label_params(params)).any?
- render_api_error!({ labels: errors }, 400)
- end
-
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
-
- if merge_request.valid?
- # Find or create labels and attach to issue
- unless params[:labels].nil?
- merge_request.remove_labels
- merge_request.add_labels_by_names(params[:labels].split(","))
- end
+ [":id/merge_request/:merge_request_id", ":id/merge_requests/:merge_request_id"].each do |path|
+ # Show MR
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - The ID of MR
+ #
+ # Example:
+ # GET /projects/:id/merge_requests/:merge_request_id
+ #
+ get path do
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+
+ authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequest
- else
- handle_merge_request_errors! merge_request.errors
end
- end
-
- # Merge MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # merge_commit_message (optional) - Custom merge commit message
- # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible
- # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds
- # Example:
- # PUT /projects/:id/merge_request/:merge_request_id/merge
- #
- put ":id/merge_request/:merge_request_id/merge" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
- # Merge request can not be merged
- # because user dont have permissions to push into target branch
- unauthorized! unless merge_request.can_be_merged_by?(current_user)
- not_allowed! if !merge_request.open? || merge_request.work_in_progress?
- merge_request.check_if_can_be_merged if merge_request.unchecked?
-
- render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged?
-
- merge_params = {
- commit_message: params[:merge_commit_message],
- should_remove_source_branch: params[:should_remove_source_branch]
- }
+ # Show MR commits
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - The ID of MR
+ #
+ # Example:
+ # GET /projects/:id/merge_requests/:merge_request_id/commits
+ #
+ get "#{path}/commits" do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+ authorize! :read_merge_request, merge_request
+ present merge_request.commits, with: Entities::RepoCommit
+ end
- if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active?
- ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params).
- execute(merge_request)
- else
- ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params).
- execute(merge_request)
+ # Show MR changes
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - The ID of MR
+ #
+ # Example:
+ # GET /projects/:id/merge_requests/:merge_request_id/changes
+ #
+ get "#{path}/changes" do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+ authorize! :read_merge_request, merge_request
+ present merge_request, with: Entities::MergeRequestChanges
end
- present merge_request, with: Entities::MergeRequest
- end
+ # Update MR
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
+ # target_branch - The target branch
+ # assignee_id - Assignee user ID
+ # title - Title of MR
+ # state_event - Status of MR. (close|reopen|merge)
+ # description - Description of MR
+ # labels (optional) - Labels for a MR as a comma-separated list
+ # milestone_id (optional) - Milestone ID
+ # Example:
+ # PUT /projects/:id/merge_requests/:merge_request_id
+ #
+ put path do
+ attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description, :milestone_id]
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ authorize! :update_merge_request, merge_request
+
+ # Ensure source_branch is not specified
+ if params[:source_branch].present?
+ render_api_error!('Source branch cannot be changed', 400)
+ end
- # Cancel Merge if Merge When build succeeds is enabled
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- #
- post ":id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ # Validate label names in advance
+ if (errors = validate_label_params(params)).any?
+ render_api_error!({ labels: errors }, 400)
+ end
- unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
- ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request)
- end
+ if merge_request.valid?
+ # Find or create labels and attach to issue
+ unless params[:labels].nil?
+ merge_request.remove_labels
+ merge_request.add_labels_by_names(params[:labels].split(","))
+ end
- # Get a merge request's comments
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # Examples:
- # GET /projects/:id/merge_request/:merge_request_id/comments
- #
- get ":id/merge_request/:merge_request_id/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ present merge_request, with: Entities::MergeRequest
+ else
+ handle_merge_request_errors! merge_request.errors
+ end
+ end
- authorize! :read_merge_request, merge_request
+ # Merge MR
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
+ # merge_commit_message (optional) - Custom merge commit message
+ # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible
+ # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds
+ # Example:
+ # PUT /projects/:id/merge_requests/:merge_request_id/merge
+ #
+ put "#{path}/merge" do
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
+ not_allowed! if !merge_request.open? || merge_request.work_in_progress?
+
+ merge_request.check_if_can_be_merged
+
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged?
+
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
+
+ if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active?
+ ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params).
+ execute(merge_request)
+ else
+ ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params).
+ execute(merge_request)
+ end
- present paginate(merge_request.notes.fresh), with: Entities::MRNote
- end
+ present merge_request, with: Entities::MergeRequest
+ end
- # Post comment to merge request
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # note (required) - Text of comment
- # Examples:
- # POST /projects/:id/merge_request/:merge_request_id/comments
- #
- post ":id/merge_request/:merge_request_id/comments" do
- required_attributes! [:note]
+ # Cancel Merge if Merge When build succeeds is enabled
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
+ #
+ post "#{path}/cancel_merge_when_build_succeeds" do
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
- authorize! :create_note, merge_request
+ ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request)
+ end
- opts = {
- note: params[:note],
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id
- }
+ # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0.
+ # Use GET "/projects/:id/merge_requests/:merge_request_id/notes" instead
+ #
+ # Get a merge request's comments
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
+ # Examples:
+ # GET /projects/:id/merge_requests/:merge_request_id/comments
+ #
+ get "#{path}/comments" do
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+
+ authorize! :read_merge_request, merge_request
+
+ present paginate(merge_request.notes.fresh), with: Entities::MRNote
+ end
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0.
+ # Use POST "/projects/:id/merge_requests/:merge_request_id/notes" instead
+ #
+ # Post comment to merge request
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
+ # note (required) - Text of comment
+ # Examples:
+ # POST /projects/:id/merge_requests/:merge_request_id/comments
+ #
+ post "#{path}/comments" do
+ required_attributes! [:note]
+
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+
+ authorize! :create_note, merge_request
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.save
+ present note, with: Entities::MRNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
- if note.save
- present note, with: Entities::MRNote
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ # List issues that will close on merge
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
+ # Examples:
+ # GET /projects/:id/merge_requests/:merge_request_id/closes_issues
+ get "#{path}/closes_issues" do
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
+ present paginate(issues), with: Entities::Issue
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 3efdfe2d46e..174473f5371 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -20,7 +20,19 @@ module API
# GET /projects/:id/snippets/:noteable_id/notes
get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
@noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"])
- present paginate(@noteable.notes), with: Entities::Note
+
+ # We exclude notes that are cross-references and that cannot be viewed
+ # by the current user. By doing this exclusion at this level and not
+ # at the DB query level (which we cannot in that case), the current
+ # page can have less elements than :per_page even if
+ # there's more than one page.
+ notes =
+ # paginate() only works with a relation. This could lead to a
+ # mismatch between the pagination headers info and the actual notes
+ # array returned, but this is really a edge-case.
+ paginate(@noteable.notes).
+ reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ present notes, with: Entities::Note
end
# Get a single +noteable+ note
@@ -35,7 +47,12 @@ module API
get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
@noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"])
@note = @noteable.notes.find(params[:note_id])
- present @note, with: Entities::Note
+
+ if @note.cross_reference_not_visible_for?(current_user)
+ not_found!("Note")
+ else
+ present @note, with: Entities::Note
+ end
end
# Create a new +noteable+ note
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index a9e0960872a..6fcb5261e40 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -3,7 +3,7 @@ module API
class Projects < Grape::API
before { authenticate! }
- resource :projects do
+ resource :projects, requirements: { id: /[^\/]+/ } do
helpers do
def map_public_to_visibility_level(attrs)
publik = attrs.delete(:public)
@@ -69,7 +69,8 @@ module API
# Example Request:
# GET /projects/:id
get ":id" do
- present user_project, with: Entities::ProjectWithAccess, user: current_user
+ present user_project, with: Entities::ProjectWithAccess, user: current_user,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
end
# Get events for a single project
@@ -98,6 +99,7 @@ module API
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - 0 by default
# import_url (optional)
+ # public_builds (optional)
# Example Request
# POST /projects
post do
@@ -114,11 +116,13 @@ module API
:namespace_id,
:public,
:visibility_level,
- :import_url]
+ :import_url,
+ :public_builds]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
- present @project, with: Entities::Project
+ present @project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, @project)
else
if @project.errors[:limit_reached].present?
error!(@project.errors[:limit_reached], 403)
@@ -143,6 +147,7 @@ module API
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional)
# import_url (optional)
+ # public_builds (optional)
# Example Request
# POST /projects/user/:user_id
post "user/:user_id" do
@@ -159,11 +164,13 @@ module API
:shared_runners_enabled,
:public,
:visibility_level,
- :import_url]
+ :import_url,
+ :public_builds]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
- present @project, with: Entities::Project
+ present @project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, @project)
else
render_validation_error!(@project)
end
@@ -182,7 +189,8 @@ module API
if @forked_project.errors.any?
conflict!(@forked_project.errors.messages)
else
- present @forked_project, with: Entities::Project
+ present @forked_project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, @forked_project)
end
end
@@ -201,6 +209,7 @@ module API
# shared_runners_enabled (optional)
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - visibility level of a project
+ # public_builds (optional)
# Example Request
# PUT /projects/:id
put ':id' do
@@ -215,7 +224,8 @@ module API
:snippets_enabled,
:shared_runners_enabled,
:public,
- :visibility_level]
+ :visibility_level,
+ :public_builds]
attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present?
@@ -229,7 +239,8 @@ module API
if user_project.errors.any?
render_validation_error!(user_project)
else
- present user_project, with: Entities::Project
+ present user_project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
end
end
@@ -241,7 +252,7 @@ module API
# DELETE /projects/:id
delete ":id" do
authorize! :remove_project, user_project
- ::Projects::DestroyService.new(user_project, current_user, {}).execute
+ ::Projects::DestroyService.new(user_project, current_user, {}).pending_delete!
end
# Mark this project as forked from another
@@ -269,7 +280,7 @@ module API
# Remove a forked_from relationship
#
# Parameters:
- # id: (required) - The ID of the project being marked as a fork
+ # id: (required) - The ID of the project being marked as a fork
# Example Request:
# DELETE /projects/:id/fork
delete ":id/fork" do
@@ -278,6 +289,43 @@ module API
user_project.forked_project_link.destroy
end
end
+
+ # Share project with group
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # group_id (required) - The ID of a group
+ # group_access (required) - Level of permissions for sharing
+ #
+ # Example Request:
+ # POST /projects/:id/share
+ post ":id/share" do
+ authorize! :admin_project, user_project
+ required_attributes! [:group_id, :group_access]
+
+ unless user_project.allowed_to_share_with_group?
+ return render_api_error!("The project sharing with group is disabled", 400)
+ end
+
+ link = user_project.project_group_links.new
+ link.group_id = params[:group_id]
+ link.group_access = params[:group_access]
+ if link.save
+ present link, with: Entities::ProjectGroupLink
+ else
+ render_api_error!(link.errors.full_messages.first, 409)
+ end
+ end
+
+ # Upload a file
+ #
+ # Parameters:
+ # id: (required) - The ID of the project
+ # file: (required) - The file to be uploaded
+ post ":id/uploads" do
+ ::Projects::UploadService.new(user_project, params[:file]).execute
+ end
+
# search for projects current_user has access to
#
# Parameters:
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index d7c48639eba..0d0f0d4616d 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -57,7 +57,7 @@ module API
not_found! "File" unless blob
content_type 'text/plain'
- present blob.data
+ header *Gitlab::Workhorse.send_git_blob(repo, blob)
end
# Get a raw blob contents by blob sha
@@ -83,7 +83,7 @@ module API
env['api.format'] = :txt
content_type blob.mime_type
- present blob.data
+ header *Gitlab::Workhorse.send_git_blob(repo, blob)
end
# Get a an archive of the repository
@@ -98,11 +98,8 @@ module API
authorize! :download_code, user_project
begin
- ArchiveRepositoryService.new(
- user_project,
- params[:sha],
- params[:format]
- ).execute
+ RepositoryArchiveCacheWorker.perform_async
+ header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format])
rescue
not_found!('File')
end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
new file mode 100644
index 00000000000..8ec91485b26
--- /dev/null
+++ b/lib/api/runners.rb
@@ -0,0 +1,175 @@
+module API
+ # Runners API
+ class Runners < Grape::API
+ before { authenticate! }
+
+ resource :runners do
+ # Get runners available for user
+ #
+ # Example Request:
+ # GET /runners
+ get do
+ runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
+ present paginate(runners), with: Entities::Runner
+ end
+
+ # Get all runners - shared and specific
+ #
+ # Example Request:
+ # GET /runners/all
+ get 'all' do
+ authenticated_as_admin!
+ runners = filter_runners(Ci::Runner.all, params[:scope])
+ present paginate(runners), with: Entities::Runner
+ end
+
+ # Get runner's details
+ #
+ # Parameters:
+ # id (required) - The ID of ther runner
+ # Example Request:
+ # GET /runners/:id
+ get ':id' do
+ runner = get_runner(params[:id])
+ authenticate_show_runner!(runner)
+
+ present runner, with: Entities::RunnerDetails, current_user: current_user
+ end
+
+ # Update runner's details
+ #
+ # Parameters:
+ # id (required) - The ID of ther runner
+ # description (optional) - Runner's description
+ # active (optional) - Runner's status
+ # tag_list (optional) - Array of tags for runner
+ # Example Request:
+ # PUT /runners/:id
+ put ':id' do
+ runner = get_runner(params[:id])
+ authenticate_update_runner!(runner)
+
+ attrs = attributes_for_keys [:description, :active, :tag_list]
+ if runner.update(attrs)
+ present runner, with: Entities::RunnerDetails, current_user: current_user
+ else
+ render_validation_error!(runner)
+ end
+ end
+
+ # Remove runner
+ #
+ # Parameters:
+ # id (required) - The ID of ther runner
+ # Example Request:
+ # DELETE /runners/:id
+ delete ':id' do
+ runner = get_runner(params[:id])
+ authenticate_delete_runner!(runner)
+ runner.destroy!
+
+ present runner, with: Entities::Runner
+ end
+ end
+
+ resource :projects do
+ before { authorize_admin_project }
+
+ # Get runners available for project
+ #
+ # Example Request:
+ # GET /projects/:id/runners
+ get ':id/runners' do
+ runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope])
+ present paginate(runners), with: Entities::Runner
+ end
+
+ # Enable runner for project
+ #
+ # Parameters:
+ # id (required) - The ID of the project
+ # runner_id (required) - The ID of the runner
+ # Example Request:
+ # POST /projects/:id/runners/:runner_id
+ post ':id/runners' do
+ required_attributes! [:runner_id]
+
+ runner = get_runner(params[:runner_id])
+ authenticate_enable_runner!(runner)
+ Ci::RunnerProject.create(runner: runner, project: user_project)
+
+ present runner, with: Entities::Runner
+ end
+
+ # Disable project's runner
+ #
+ # Parameters:
+ # id (required) - The ID of the project
+ # runner_id (required) - The ID of the runner
+ # Example Request:
+ # DELETE /projects/:id/runners/:runner_id
+ delete ':id/runners/:runner_id' do
+ runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
+ not_found!('Runner') unless runner_project
+
+ runner = runner_project.runner
+ forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
+
+ runner_project.destroy
+
+ present runner, with: Entities::Runner
+ end
+ end
+
+ helpers do
+ def filter_runners(runners, scope, options = {})
+ return runners unless scope.present?
+
+ available_scopes = ::Ci::Runner::AVAILABLE_SCOPES
+ if options[:without]
+ available_scopes = available_scopes - options[:without]
+ end
+
+ if (available_scopes & [scope]).empty?
+ render_api_error!('Scope contains invalid value', 400)
+ end
+
+ runners.send(scope)
+ end
+
+ def get_runner(id)
+ runner = Ci::Runner.find(id)
+ not_found!('Runner') unless runner
+ runner
+ end
+
+ def authenticate_show_runner!(runner)
+ return if runner.is_shared || current_user.is_admin?
+ forbidden!("No access granted") unless user_can_access_runner?(runner)
+ end
+
+ def authenticate_update_runner!(runner)
+ return if current_user.is_admin?
+ forbidden!("Runner is shared") if runner.is_shared?
+ forbidden!("No access granted") unless user_can_access_runner?(runner)
+ end
+
+ def authenticate_delete_runner!(runner)
+ return if current_user.is_admin?
+ forbidden!("Runner is shared") if runner.is_shared?
+ forbidden!("Runner associated with more than one project") if runner.projects.count > 1
+ forbidden!("No access granted") unless user_can_access_runner?(runner)
+ end
+
+ def authenticate_enable_runner!(runner)
+ forbidden!("Runner is shared") if runner.is_shared?
+ return if current_user.is_admin?
+ forbidden!("No access granted") unless user_can_access_runner?(runner)
+ end
+
+ def user_can_access_runner?(runner)
+ current_user.ci_authorized_runners.exists?(runner.id)
+ end
+ end
+ end
+end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 47621f443e6..2d8a9e51bb9 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -40,6 +40,27 @@ module API
end
end
+ # Delete tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # Example Request:
+ # DELETE /projects/:id/repository/tags/:tag
+ delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.*/ } do
+ authorize_push_project
+ result = DeleteTagService.new(user_project, current_user).
+ execute(params[:tag_name])
+
+ if result[:status] == :success
+ {
+ tag_name: params[:tag_name]
+ }
+ else
+ render_api_error!(result[:message], result[:return_code])
+ end
+ end
+
# Add release notes to tag
#
# Parameters:
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 2781f1cf191..d1d07394e92 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -43,6 +43,75 @@ module API
render_api_error!(errors, 400)
end
end
+
+ # Get triggers list
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # page (optional) - The page number for pagination
+ # per_page (optional) - The value of items per page to show
+ # Example Request:
+ # GET /projects/:id/triggers
+ get ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ triggers = user_project.triggers.includes(:trigger_requests)
+ triggers = paginate(triggers)
+
+ present triggers, with: Entities::Trigger
+ end
+
+ # Get specific trigger of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # token (required) - The `token` of a trigger
+ # Example Request:
+ # GET /projects/:id/triggers/:token
+ get ':id/triggers/:token' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ present trigger, with: Entities::Trigger
+ end
+
+ # Create trigger
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # POST /projects/:id/triggers
+ post ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.create
+
+ present trigger, with: Entities::Trigger
+ end
+
+ # Delete trigger
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # token (required) - The `token` of a trigger
+ # Example Request:
+ # DELETE /projects/:id/triggers/:token
+ delete ':id/triggers/:token' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ trigger.destroy
+
+ present trigger, with: Entities::Trigger
+ end
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 3400f0713ef..13ab17c6904 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -39,7 +39,7 @@ module API
if current_user.is_admin?
present @user, with: Entities::UserFull
else
- present @user, with: Entities::UserBasic
+ present @user, with: Entities::User
end
end
@@ -61,19 +61,20 @@ module API
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
# confirm - Require user confirmation - true (default) or false
+ # external - Flags the user as external - true or false(default)
# Example Request:
# POST /users
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external]
admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
user = User.build_user(attrs)
user.admin = admin unless admin.nil?
user.skip_confirmation! unless confirm
-
identity_attrs = attributes_for_keys [:provider, :extern_uid]
+
if identity_attrs.any?
user.identities.build(identity_attrs)
end
@@ -107,12 +108,13 @@ module API
# bio - Bio
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
+ # external - Flags the user as external - true or false(default)
# Example Request:
# PUT /users/:id
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external]
user = User.find(params[:id])
not_found!('User') unless user
@@ -284,10 +286,12 @@ module API
authenticated_as_admin!
user = User.find_by(id: params[:id])
- if user
+ if !user
+ not_found!('User')
+ elsif !user.ldap_blocked?
user.block
else
- not_found!('User')
+ forbidden!('LDAP blocked users cannot be modified by the API')
end
end
@@ -299,10 +303,12 @@ module API
authenticated_as_admin!
user = User.find_by(id: params[:id])
- if user
- user.activate
- else
+ if !user
not_found!('User')
+ elsif user.ldap_blocked?
+ forbidden!('LDAP blocked users cannot be unblocked by the API')
+ else
+ user.activate
end
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
new file mode 100644
index 00000000000..f6495071a11
--- /dev/null
+++ b/lib/api/variables.rb
@@ -0,0 +1,95 @@
+module API
+ # Projects variables API
+ class Variables < Grape::API
+ before { authenticate! }
+ before { authorize! :admin_build, user_project }
+
+ resource :projects do
+ # Get project variables
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # page (optional) - The page number for pagination
+ # per_page (optional) - The value of items per page to show
+ # Example Request:
+ # GET /projects/:id/variables
+ get ':id/variables' do
+ variables = user_project.variables
+ present paginate(variables), with: Entities::Variable
+ end
+
+ # Get specific variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The `key` of variable
+ # Example Request:
+ # GET /projects/:id/variables/:key
+ get ':id/variables/:key' do
+ key = params[:key]
+ variable = user_project.variables.find_by(key: key.to_s)
+
+ return not_found!('Variable') unless variable
+
+ present variable, with: Entities::Variable
+ end
+
+ # Create a new variable in project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The key of variable
+ # value (required) - The value of variable
+ # Example Request:
+ # POST /projects/:id/variables
+ post ':id/variables' do
+ required_attributes! [:key, :value]
+
+ variable = user_project.variables.create(key: params[:key], value: params[:value])
+
+ if variable.valid?
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ # Update existing variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (optional) - The `key` of variable
+ # value (optional) - New value for `value` field of variable
+ # Example Request:
+ # PUT /projects/:id/variables/:key
+ put ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key].to_s)
+
+ return not_found!('Variable') unless variable
+
+ attrs = attributes_for_keys [:value]
+ if variable.update(attrs)
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ # Delete existing variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The ID of a variable
+ # Example Request:
+ # DELETE /projects/:id/variables/:key
+ delete ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key].to_s)
+
+ return not_found!('Variable') unless variable
+ variable.destroy
+
+ present variable, with: Entities::Variable
+ end
+ end
+ end
+end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 099062eeb8b..4962f5e53ce 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,6 +1,9 @@
module Backup
class Manager
def pack
+ # Make sure there is a connection
+ ActiveRecord::Base.connection.reconnect!
+
# saving additional informations
s = {}
s[:db_version] = "#{ActiveRecord::Migrator.current_version}"
diff --git a/lib/banzai.rb b/lib/banzai.rb
index 093382261ae..b467413a7dd 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -7,6 +7,10 @@ module Banzai
Renderer.render_result(text, context)
end
+ def self.pre_process(text, context)
+ Renderer.pre_process(text, context)
+ end
+
def self.post_process(html, context)
Renderer.post_process(html, context)
end
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index ba2866e1efa..0257848b6bc 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
# Common methods for ReferenceFilters that support an optional cross-project
# reference.
diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb
index fd4fe024252..905c4c0144e 100644
--- a/lib/banzai/filter.rb
+++ b/lib/banzai/filter.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/string/output_safety'
-require 'banzai'
module Banzai
module Filter
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 63ad8910c0f..34c38913474 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# Issues, Merge Requests, Snippets, Commits and Commit Ranges share
@@ -47,7 +45,17 @@ module Banzai
{ object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
end
- delegate :object_class, :object_sym, :references_in, to: :class
+ def object_class
+ self.class.object_class
+ end
+
+ def object_sym
+ self.class.object_sym
+ end
+
+ def references_in(*args, &block)
+ self.class.references_in(*args, &block)
+ end
def find_object(project, id)
# Implement in child class
@@ -60,28 +68,34 @@ module Banzai
end
def call
- # `#123`
- replace_text_nodes_matching(object_class.reference_pattern) do |content|
- object_link_filter(content, object_class.reference_pattern)
- end
+ if object_class.reference_pattern
+ # `#123`
+ replace_text_nodes_matching(object_class.reference_pattern) do |content|
+ object_link_filter(content, object_class.reference_pattern)
+ end
- # `[Issue](#123)`, which is turned into
- # `<a href="#123">Issue</a>`
- replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
- object_link_filter(link, object_class.reference_pattern, link_text: text)
+ # `[Issue](#123)`, which is turned into
+ # `<a href="#123">Issue</a>`
+ replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
+ object_link_filter(link, object_class.reference_pattern, link_text: text)
+ end
end
- # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
- # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
- replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
- object_link_filter(text, object_class.link_reference_pattern)
- end
+ if object_class.link_reference_pattern
+ # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
+ # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
+ replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
+ object_link_filter(text, object_class.link_reference_pattern)
+ end
- # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
- # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
- replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
- object_link_filter(link, object_class.link_reference_pattern, link_text: text)
+ # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
+ # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
+ replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
+ object_link_filter(link, object_class.link_reference_pattern, link_text: text)
+ end
end
+
+ doc
end
# Replace references (like `!123` for merge requests) in text with links
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index da4ee80c1b5..856f56fb175 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index e67cd45ab9b..470727ee312 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces commit range references with links.
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 9e57608b483..713a56ba949 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces commit references with links.
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index 86838e1483c..207437ba7cf 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,5 +1,4 @@
require 'action_controller'
-require 'banzai'
require 'gitlab_emoji'
require 'html/pipeline/filter'
@@ -46,7 +45,8 @@ module Banzai
private
def emoji_url(name)
- emoji_path = "emoji/#{emoji_filename(name)}"
+ emoji_path = emoji_filename(name)
+
if context[:asset_host]
# Asset host is specified.
url_to_image(emoji_path)
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index 6136e73c096..edc26386903 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces external issue tracker references with links.
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index ac87b9820af..8d368f3b9e7 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
new file mode 100644
index 00000000000..f31f921903b
--- /dev/null
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -0,0 +1,174 @@
+require 'banzai'
+require 'html/pipeline/filter'
+
+module Banzai
+ module Filter
+ # HTML Filter for parsing Gollum's tags in HTML. It's only parses the
+ # following tags:
+ #
+ # - Link to internal pages:
+ #
+ # * [[Bug Reports]]
+ # * [[How to Contribute|Contributing]]
+ #
+ # - Link to external resources:
+ #
+ # * [[http://en.wikipedia.org/wiki/Git_(software)]]
+ # * [[Git|http://en.wikipedia.org/wiki/Git_(software)]]
+ #
+ # - Link internal images, the special attributes will be ignored:
+ #
+ # * [[images/logo.png]]
+ # * [[images/logo.png|alt=Logo]]
+ #
+ # - Link external images, the special attributes will be ignored:
+ #
+ # * [[http://example.com/images/logo.png]]
+ # * [[http://example.com/images/logo.png|alt=Logo]]
+ #
+ # - Insert a Table of Contents list:
+ #
+ # * [[_TOC_]]
+ #
+ # Based on Gollum::Filter::Tags
+ #
+ # Context options:
+ # :project_wiki (required) - Current project wiki.
+ #
+ class GollumTagsFilter < HTML::Pipeline::Filter
+ include ActionView::Helpers::TagHelper
+
+ # Pattern to match tags content that should be parsed in HTML.
+ #
+ # Gollum's tags have been made to resemble the tags of other markups,
+ # especially MediaWiki. The basic syntax is:
+ #
+ # [[tag]]
+ #
+ # Some tags will accept attributes which are separated by pipe
+ # symbols.Some attributes must precede the tag and some must follow it:
+ #
+ # [[prefix-attribute|tag]]
+ # [[tag|suffix-attribute]]
+ #
+ # See https://github.com/gollum/gollum/wiki
+ #
+ # Rubular: http://rubular.com/r/7dQnE5CUCH
+ TAGS_PATTERN = %r{\[\[(.+?)\]\]}.freeze
+
+ # Pattern to match allowed image extensions
+ ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i.freeze
+
+ def call
+ search_text_nodes(doc).each do |node|
+ # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
+ # before this one, it will be converted into `[[<em>TOC</em>]]`, so it
+ # needs special-case handling
+ if toc_tag?(node)
+ process_toc_tag(node)
+ else
+ content = node.content
+
+ next unless content =~ TAGS_PATTERN
+
+ html = process_tag($1)
+
+ if html && html != node.content
+ node.replace(html)
+ end
+ end
+ end
+
+ doc
+ end
+
+ private
+
+ # Replace an entire `[[<em>TOC</em>]]` node with the result generated by
+ # TableOfContentsFilter
+ def process_toc_tag(node)
+ node.parent.parent.replace(result[:toc].presence || '')
+ end
+
+ # Process a single tag into its final HTML form.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML version of the tag.
+ def process_tag(tag)
+ parts = tag.split('|')
+
+ return if parts.size.zero?
+
+ process_image_tag(parts) || process_page_link_tag(parts)
+ end
+
+ # Attempt to process the tag as an image tag.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML if the tag is a valid image tag or nil
+ # if it is not.
+ def process_image_tag(parts)
+ content = parts[0].strip
+
+ return unless image?(content)
+
+ if url?(content)
+ path = content
+ elsif file = project_wiki.find_file(content)
+ path = ::File.join project_wiki_base_path, file.path
+ end
+
+ if path
+ content_tag(:img, nil, src: path)
+ end
+ end
+
+ def toc_tag?(node)
+ node.content == 'TOC' &&
+ node.parent.name == 'em' &&
+ node.parent.parent.text == '[[TOC]]'
+ end
+
+ def image?(path)
+ path =~ ALLOWED_IMAGE_EXTENSIONS
+ end
+
+ def url?(path)
+ path.start_with?(*%w(http https))
+ end
+
+ # Attempt to process the tag as a page link tag.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML if the tag is a valid page link tag or nil
+ # if it is not.
+ def process_page_link_tag(parts)
+ if parts.size == 1
+ url = parts[0].strip
+ else
+ name, url = *parts.compact.map(&:strip)
+ end
+
+ content_tag(:a, name || url, href: url)
+ end
+
+ def project_wiki
+ context[:project_wiki]
+ end
+
+ def project_wiki_base_path
+ project_wiki && project_wiki.wiki_base_path
+ end
+
+ # Ensure that a :project_wiki key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project_wiki
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 51180cb901a..2732e0b5145 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces issue references with links. References to
@@ -11,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 a3a7a23c1e6..8147e5ed3c7 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -1,80 +1,47 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces label references with links.
- class LabelReferenceFilter < ReferenceFilter
- # Public: Find label references in text
- #
- # LabelReferenceFilter.references_in(text) do |match, id, name|
- # "<a href=...>#{Label.find(id)}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, an optional Integer label ID, and an optional
- # String label name.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Label.reference_pattern) do |match|
- yield match, $~[:label_id].to_i, $~[:label_name]
- end
+ class LabelReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Label
end
- def self.referenced_by(node)
- { label: LazyReference.new(Label, node.attr("data-label")) }
+ def find_object(project, id)
+ project.labels.find(id)
end
- def call
- replace_text_nodes_matching(Label.reference_pattern) do |content|
- label_link_filter(content)
- end
-
- replace_link_nodes_with_href(Label.reference_pattern) do |link, text|
- label_link_filter(link, link_text: text)
+ def self.references_in(text, pattern = Label.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~
end
end
- # Replace label references in text with links to the label specified.
- #
- # text - String text to replace references in.
- #
- # Returns a String with label references replaced with links. All links
- # have `gfm` and `gfm-label` class names attached for styling.
- def label_link_filter(text, link_text: nil)
- project = context[:project]
-
- self.class.references_in(text) do |match, id, name|
- params = label_params(id, name)
-
- if label = project.labels.find_by(params)
- url = url_for_label(project, label)
- klass = reference_class(:label)
- data = data_attribute(
- original: link_text || match,
- project: project.id,
- label: label.id
- )
+ def references_in(text, pattern = Label.reference_pattern)
+ text.gsub(pattern) do |match|
+ project = project_from_ref($~[:project])
+ params = label_params($~[:label_id].to_i, $~[:label_name])
+ label = project.labels.find_by(params)
- text = link_text || render_colored_label(label)
-
- %(<a href="#{url}" #{data}
- class="#{klass}">#{escape_once(text)}</a>)
+ if label
+ yield match, label.id, $~[:project], $~
else
match
end
end
end
- def url_for_label(project, label)
+ def url_for_object(label, project)
h = Gitlab::Application.routes.url_helpers
- h.namespace_project_issues_url( project.namespace, project, label_name: label.name,
- only_path: context[:only_path])
+ h.namespace_project_issues_url(project.namespace, project, label_name: label.name,
+ only_path: context[:only_path])
end
- def render_colored_label(label)
- LabelsHelper.render_colored_label(label)
+ def object_link_text(object, matches)
+ if context[:project] == object.project
+ LabelsHelper.render_colored_label(object)
+ else
+ LabelsHelper.render_colored_cross_project_label(object)
+ end
end
# Parameters to pass to `Label.find_by` based on the given arguments
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index d09cf41df39..0659fed1419 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index 755b946a34b..57c71708992 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces merge request references with links. References
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
new file mode 100644
index 00000000000..e88b27c1fae
--- /dev/null
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -0,0 +1,22 @@
+require 'banzai'
+
+module Banzai
+ module Filter
+ # HTML filter that replaces milestone references with links.
+ class MilestoneReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Milestone
+ end
+
+ def find_object(project, id)
+ project.milestones.find_by(iid: id)
+ end
+
+ def url_for_object(issue, project)
+ h = Gitlab::Application.routes.url_helpers
+ h.namespace_project_milestone_url(project.namespace, project, milestone,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index f01a32b5ae5..7141ed7c9bd 100644
--- a/lib/banzai/filter/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
@@ -10,7 +9,7 @@ module Banzai
#
class RedactorFilter < HTML::Pipeline::Filter
def call
- doc.css('a.gfm').each do |node|
+ Querying.css(doc, 'a.gfm').each do |node|
unless user_can_see_reference?(node)
# The reference should be replaced by the original text,
# which is not always the same as the rendered text.
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 8ca05ace88c..3637b1bac94 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/string/output_safety'
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
@@ -124,7 +123,7 @@ module Banzai
def replace_link_nodes_with_text(pattern)
return doc if project.nil?
- doc.search('a').each do |node|
+ doc.xpath('descendant-or-self::a').each do |node|
klass = node.attr('class')
next if klass && klass.include?('gfm')
@@ -133,7 +132,8 @@ module Banzai
next unless link && text
- link = URI.decode(link)
+ link = CGI.unescape(link)
+ next unless link.force_encoding('UTF-8').valid_encoding?
# Ignore ending punctionation like periods or commas
next unless link == text && text =~ /\A#{pattern}/
@@ -162,7 +162,7 @@ module Banzai
def replace_link_nodes_with_href(pattern)
return doc if project.nil?
- doc.search('a').each do |node|
+ doc.xpath('descendant-or-self::a').each do |node|
klass = node.attr('class')
next if klass && klass.include?('gfm')
@@ -170,7 +170,8 @@ module Banzai
text = node.text
next unless link && text
- link = URI.decode(link)
+ link = CGI.unescape(link)
+ next unless link.force_encoding('UTF-8').valid_encoding?
next unless link && link =~ /\A#{pattern}\z/
html = yield link, text
diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb
index 12412ff7ea9..86d484feb90 100644
--- a/lib/banzai/filter/reference_gatherer_filter.rb
+++ b/lib/banzai/filter/reference_gatherer_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
@@ -16,7 +15,7 @@ module Banzai
end
def call
- doc.css('a.gfm').each do |node|
+ Querying.css(doc, 'a.gfm').each do |node|
gather_references(node)
end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 5a081125f21..41380627d39 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
@@ -91,7 +90,7 @@ module Banzai
parts = request_path.split('/')
parts.pop if path_type(request_path) != 'tree'
- while parts.length > 1 && path.start_with?('../')
+ while path.start_with?('../')
parts.pop
path.sub!('../', '')
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index d03e3ae4b3c..e8011519608 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'html/pipeline/sanitization_filter'
@@ -8,15 +7,10 @@ module Banzai
#
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
class SanitizationFilter < HTML::Pipeline::SanitizationFilter
+ UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
+
def whitelist
- # Descriptions are more heavily sanitized, allowing only a few elements.
- # See http://git.io/vkuAN
- if context[:inline_sanitization]
- whitelist = LIMITED
- whitelist[:elements] -= %w(pre code img ol ul li)
- else
- whitelist = super
- end
+ whitelist = super
customize_whitelist(whitelist)
@@ -44,11 +38,15 @@ module Banzai
# Allow span elements
whitelist[:elements].push('span')
+ # Allow abbr elements with title attribute
+ whitelist[:elements].push('abbr')
+ whitelist[:attributes]['abbr'] = %w(title)
+
# Allow any protocol in `a` elements...
whitelist[:protocols].delete('a')
- # ...but then remove links with the `javascript` protocol
- whitelist[:transformers].push(remove_javascript_links)
+ # ...but then remove links with unsafe protocols
+ whitelist[:transformers].push(remove_unsafe_links)
# Remove `rel` attribute from `a` elements
whitelist[:transformers].push(remove_rel)
@@ -59,14 +57,19 @@ module Banzai
whitelist
end
- def remove_javascript_links
+ def remove_unsafe_links
lambda do |env|
node = env[:node]
return unless node.name == 'a'
return unless node.has_attribute?('href')
- if node['href'].start_with?('javascript', ':javascript')
+ begin
+ uri = Addressable::URI.parse(node['href'])
+ uri.scheme.strip! if uri.scheme
+
+ node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
+ rescue Addressable::URI::InvalidURIError
node.remove_attribute('href')
end
end
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
index 1ad5df96f85..c870a42f741 100644
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ b/lib/banzai/filter/snippet_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces snippet references with links. References to
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index c889cc1e97c..8c5855e5ffc 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet'
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 9b3e67206d5..4056dcd6d64 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index bdf7c2ebdfc..66608c9859c 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'task_list/filter'
module Banzai
@@ -12,13 +11,18 @@ module Banzai
#
# See https://github.com/github/task_list/pull/60
class TaskListFilter < TaskList::Filter
- def add_css_class(node, *new_class_names)
+ def add_css_class_with_fix(node, *new_class_names)
if new_class_names.include?('task-list')
- super if node.children.any? { |c| c['class'] == 'task-list-item' }
- else
- super
+ # Don't add class to all lists
+ return
+ elsif new_class_names.include?('task-list-item')
+ add_css_class_without_fix(node.parent, 'task-list')
end
+
+ add_css_class_without_fix(node, *new_class_names)
end
+
+ alias_method_chain :add_css_class, :fix
end
end
end
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index 1a1d0aad8ca..f642aee0967 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 964ab60f614..24f16f8b547 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces user or group references with links.
diff --git a/lib/banzai/filter/yaml_front_matter_filter.rb b/lib/banzai/filter/yaml_front_matter_filter.rb
new file mode 100644
index 00000000000..e4e2f3f228d
--- /dev/null
+++ b/lib/banzai/filter/yaml_front_matter_filter.rb
@@ -0,0 +1,28 @@
+require 'html/pipeline/filter'
+require 'yaml'
+
+module Banzai
+ module Filter
+ class YamlFrontMatterFilter < HTML::Pipeline::Filter
+ DELIM = '---'.freeze
+
+ # Hat-tip to Middleman: https://git.io/v2e0z
+ PATTERN = %r{
+ \A(?:[^\r\n]*coding:[^\r\n]*\r?\n)?
+ (?<start>#{DELIM})[ ]*\r?\n
+ (?<frontmatter>.*?)[ ]*\r?\n?
+ ^(?<stop>#{DELIM})[ ]*\r?\n?
+ \r?\n?
+ (?<content>.*)
+ }mx.freeze
+
+ def call
+ match = PATTERN.match(html)
+
+ return html unless match
+
+ "```yaml\n#{match['frontmatter']}\n```\n\n#{match['content']}"
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter_array.rb b/lib/banzai/filter_array.rb
new file mode 100644
index 00000000000..77835a14027
--- /dev/null
+++ b/lib/banzai/filter_array.rb
@@ -0,0 +1,27 @@
+module Banzai
+ class FilterArray < Array
+ # Insert a value immediately after another value
+ #
+ # If the preceding value does not exist, the new value is added to the end
+ # of the Array.
+ def insert_after(after_value, value)
+ i = index(after_value) || length - 1
+
+ insert(i + 1, value)
+ end
+
+ # Insert a value immediately before another value
+ #
+ # If the succeeding value does not exist, the new value is added to the
+ # beginning of the Array.
+ def insert_before(before_value, value)
+ i = index(before_value) || -1
+
+ if i < 0
+ unshift(value)
+ else
+ insert(i, value)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/lazy_reference.rb b/lib/banzai/lazy_reference.rb
index 073ec5d9801..1095b4debc7 100644
--- a/lib/banzai/lazy_reference.rb
+++ b/lib/banzai/lazy_reference.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
class LazyReference
def self.load(refs)
diff --git a/lib/banzai/pipeline.rb b/lib/banzai/pipeline.rb
index 4e017809d9d..142a9962eb1 100644
--- a/lib/banzai/pipeline.rb
+++ b/lib/banzai/pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
def self.[](name)
diff --git a/lib/banzai/pipeline/asciidoc_pipeline.rb b/lib/banzai/pipeline/asciidoc_pipeline.rb
deleted file mode 100644
index 5e76a817be5..00000000000
--- a/lib/banzai/pipeline/asciidoc_pipeline.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'banzai'
-
-module Banzai
- module Pipeline
- class AsciidocPipeline < BasePipeline
- def self.filters
- [
- Filter::RelativeLinkFilter
- ]
- end
- end
- end
-end
diff --git a/lib/banzai/pipeline/atom_pipeline.rb b/lib/banzai/pipeline/atom_pipeline.rb
index 957f352aec5..9694e4bc23f 100644
--- a/lib/banzai/pipeline/atom_pipeline.rb
+++ b/lib/banzai/pipeline/atom_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class AtomPipeline < FullPipeline
diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb
index cd30009e5c0..f60966c3c0f 100644
--- a/lib/banzai/pipeline/base_pipeline.rb
+++ b/lib/banzai/pipeline/base_pipeline.rb
@@ -1,11 +1,10 @@
-require 'banzai'
require 'html/pipeline'
module Banzai
module Pipeline
class BasePipeline
def self.filters
- []
+ FilterArray[]
end
def self.transform_context(context)
diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb
new file mode 100644
index 00000000000..adc09c8afbd
--- /dev/null
+++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb
@@ -0,0 +1,16 @@
+module Banzai
+ module Pipeline
+ class BroadcastMessagePipeline < DescriptionPipeline
+ def self.filters
+ @filters ||= FilterArray[
+ Filter::MarkdownFilter,
+ Filter::SanitizationFilter,
+
+ Filter::EmojiFilter,
+ Filter::AutolinkFilter,
+ Filter::ExternalLinkFilter
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/combined_pipeline.rb b/lib/banzai/pipeline/combined_pipeline.rb
index f3bf1809d18..60190f8d9dd 100644
--- a/lib/banzai/pipeline/combined_pipeline.rb
+++ b/lib/banzai/pipeline/combined_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
module CombinedPipeline
@@ -12,7 +10,7 @@ module Banzai
end
def self.filters
- pipelines.flat_map(&:filters)
+ FilterArray.new(pipelines.flat_map(&:filters))
end
def self.transform_context(context)
diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb
index 94c2cb165a5..f2395867658 100644
--- a/lib/banzai/pipeline/description_pipeline.rb
+++ b/lib/banzai/pipeline/description_pipeline.rb
@@ -1,14 +1,23 @@
-require 'banzai'
-
module Banzai
module Pipeline
class DescriptionPipeline < FullPipeline
def self.transform_context(context)
super(context).merge(
# SanitizationFilter
- inline_sanitization: true
+ whitelist: whitelist
)
end
+
+ private
+
+ def self.whitelist
+ # Descriptions are more heavily sanitized, allowing only a few elements.
+ # See http://git.io/vkuAN
+ whitelist = Banzai::Filter::SanitizationFilter::LIMITED
+ whitelist[:elements] -= %w(pre code img ol ul li)
+
+ whitelist
+ end
end
end
end
diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb
index 14356145a35..e47c384afc1 100644
--- a/lib/banzai/pipeline/email_pipeline.rb
+++ b/lib/banzai/pipeline/email_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class EmailPipeline < FullPipeline
diff --git a/lib/banzai/pipeline/full_pipeline.rb b/lib/banzai/pipeline/full_pipeline.rb
index 72395a5d50e..d47ddfda4be 100644
--- a/lib/banzai/pipeline/full_pipeline.rb
+++ b/lib/banzai/pipeline/full_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline)
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 38750b55ec7..8cd4b50e65a 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -1,10 +1,8 @@
-require 'banzai'
-
module Banzai
module Pipeline
class GfmPipeline < BasePipeline
def self.filters
- @filters ||= [
+ @filters ||= FilterArray[
Filter::SyntaxHighlightFilter,
Filter::SanitizationFilter,
@@ -22,6 +20,7 @@ module Banzai
Filter::CommitRangeReferenceFilter,
Filter::CommitReferenceFilter,
Filter::LabelReferenceFilter,
+ Filter::MilestoneReferenceFilter,
Filter::TaskListFilter
]
diff --git a/lib/banzai/pipeline/note_pipeline.rb b/lib/banzai/pipeline/note_pipeline.rb
index 89335143852..7890f20f716 100644
--- a/lib/banzai/pipeline/note_pipeline.rb
+++ b/lib/banzai/pipeline/note_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class NotePipeline < FullPipeline
diff --git a/lib/banzai/pipeline/plain_markdown_pipeline.rb b/lib/banzai/pipeline/plain_markdown_pipeline.rb
index 998fd75daa2..3f45db21869 100644
--- a/lib/banzai/pipeline/plain_markdown_pipeline.rb
+++ b/lib/banzai/pipeline/plain_markdown_pipeline.rb
@@ -1,10 +1,8 @@
-require 'banzai'
-
module Banzai
module Pipeline
class PlainMarkdownPipeline < BasePipeline
def self.filters
- [
+ FilterArray[
Filter::MarkdownFilter
]
end
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index 148f24b6ce1..ecff094b1e5 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -1,10 +1,8 @@
-require 'banzai'
-
module Banzai
module Pipeline
class PostProcessPipeline < BasePipeline
def self.filters
- [
+ FilterArray[
Filter::RelativeLinkFilter,
Filter::RedactorFilter
]
diff --git a/lib/banzai/pipeline/pre_process_pipeline.rb b/lib/banzai/pipeline/pre_process_pipeline.rb
new file mode 100644
index 00000000000..50dc978b452
--- /dev/null
+++ b/lib/banzai/pipeline/pre_process_pipeline.rb
@@ -0,0 +1,17 @@
+module Banzai
+ module Pipeline
+ class PreProcessPipeline < BasePipeline
+ def self.filters
+ FilterArray[
+ Filter::YamlFrontMatterFilter
+ ]
+ end
+
+ def self.transform_context(context)
+ context.merge(
+ pre_process: true
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/reference_extraction_pipeline.rb b/lib/banzai/pipeline/reference_extraction_pipeline.rb
index 4f9bc9fcccc..919998380e4 100644
--- a/lib/banzai/pipeline/reference_extraction_pipeline.rb
+++ b/lib/banzai/pipeline/reference_extraction_pipeline.rb
@@ -1,10 +1,8 @@
-require 'banzai'
-
module Banzai
module Pipeline
class ReferenceExtractionPipeline < BasePipeline
def self.filters
- [
+ FilterArray[
Filter::ReferenceGathererFilter
]
end
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index 6725c9039a9..ba2555df98d 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -1,9 +1,23 @@
-require 'banzai'
-
module Banzai
module Pipeline
class SingleLinePipeline < GfmPipeline
+ def self.filters
+ @filters ||= FilterArray[
+ Filter::SanitizationFilter,
+
+ Filter::EmojiFilter,
+ Filter::AutolinkFilter,
+ Filter::ExternalLinkFilter,
+ Filter::UserReferenceFilter,
+ Filter::IssueReferenceFilter,
+ Filter::ExternalIssueReferenceFilter,
+ Filter::MergeRequestReferenceFilter,
+ Filter::SnippetReferenceFilter,
+ Filter::CommitRangeReferenceFilter,
+ Filter::CommitReferenceFilter,
+ ]
+ end
end
end
end
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
new file mode 100644
index 00000000000..9b4ff0f0f80
--- /dev/null
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -0,0 +1,12 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class WikiPipeline < FullPipeline
+ def self.filters
+ @filters ||= super.insert_after(Filter::TableOfContentsFilter,
+ Filter::GollumTagsFilter)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
new file mode 100644
index 00000000000..1e1b51e683e
--- /dev/null
+++ b/lib/banzai/querying.rb
@@ -0,0 +1,18 @@
+module Banzai
+ module Querying
+ # Searches a Nokogiri document using a CSS query, optionally optimizing it
+ # whenever possible.
+ #
+ # document - A document/element to search.
+ # query - The CSS query to use.
+ #
+ # Returns a Nokogiri::XML::NodeSet.
+ def self.css(document, query)
+ # When using "a.foo" Nokogiri compiles this to "//a[...]" but
+ # "descendant::a[...]" is quite a bit faster and achieves the same result.
+ xpath = Nokogiri::CSS.xpath_for(query)[0].gsub(%r{^//}, 'descendant::')
+
+ document.xpath(xpath)
+ end
+ end
+end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index 2c197d31898..f4079538ec5 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 115ae914524..ae714c87dc5 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -1,7 +1,5 @@
module Banzai
module Renderer
- CACHE_ENABLED = false
-
# Convert a Markdown String into an HTML-safe String of HTML
#
# Note that while the returned HTML will have been sanitized of dangerous
@@ -20,7 +18,7 @@ module Banzai
cache_key = context.delete(:cache_key)
cache_key = full_cache_key(cache_key, context[:pipeline])
- if cache_key && CACHE_ENABLED
+ if cache_key
Rails.cache.fetch(cache_key) do
cacheless_render(text, context)
end
@@ -33,6 +31,12 @@ module Banzai
Pipeline[context[:pipeline]].call(text, context)
end
+ def self.pre_process(text, context)
+ pipeline = Pipeline[:pre_process]
+
+ pipeline.to_html(text, context)
+ end
+
# Perform post-processing on an HTML String
#
# This method is used to perform state-dependent changes to a String of
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 5c347e432b4..4e85d2c3c74 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -25,7 +25,7 @@ module Ci
format :json
- helpers Helpers
+ helpers ::Ci::API::Helpers
helpers ::API::Helpers
helpers Gitlab::CurrentSettings
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 15faa6edd84..2e9a5d311f9 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -13,14 +13,14 @@ module Ci
post "register" do
authenticate_runner!
update_runner_last_contact
+ update_runner_info
required_attributes! [:token]
not_found! unless current_runner.active?
build = Ci::RegisterBuildService.new.execute(current_runner)
if build
- update_runner_info
- present build, with: Entities::Build
+ present build, with: Entities::BuildDetails
else
not_found!
end
@@ -38,6 +38,8 @@ module Ci
authenticate_runner!
update_runner_last_contact
build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
+ forbidden!('Build has been erased!') if build.erased?
+
build.update_attributes(trace: params[:trace]) if params[:trace]
case params[:state].to_s
@@ -78,11 +80,13 @@ module Ci
# Parameters:
# id (required) - The ID of a build
# token (required) - The build authorization token
- # file (required) - The uploaded file
+ # file (required) - Artifacts file
# Parameters (accelerated by GitLab Workhorse):
# file.path - path to locally stored body (generated by Workhorse)
# file.name - real filename as send in Content-Disposition
# file.type - real content type as send in Content-Type
+ # metadata.path - path to locally stored body (generated by Workhorse)
+ # metadata.name - filename (generated by Workhorse)
# Headers:
# BUILD-TOKEN (required) - The build authorization token, the same as token
# Body:
@@ -96,13 +100,21 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
authenticate_build_token!(build)
- forbidden!('build is not running') unless build.running?
+ forbidden!('Build is not running!') unless build.running?
+ forbidden!('Build has been erased!') if build.erased?
+
+ artifacts_upload_path = ArtifactUploader.artifacts_upload_path
+ artifacts = uploaded_file(:file, artifacts_upload_path)
+ metadata = uploaded_file(:metadata, artifacts_upload_path)
- file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path)
- file_to_large! unless file.size < max_artifacts_size
+ bad_request!('Missing artifacts file!') unless artifacts
+ file_to_large! unless artifacts.size < max_artifacts_size
- if build.update_attributes(artifacts_file: file)
- present build, with: Entities::Build
+ build.artifacts_file = artifacts
+ build.artifacts_metadata = metadata
+
+ if build.save
+ present(build, with: Entities::BuildDetails)
else
render_validation_error!(build)
end
@@ -134,7 +146,7 @@ module Ci
present_file!(artifacts_file.path, artifacts_file.filename)
end
- # Remove the artifacts file from build
+ # Remove the artifacts file from build - Runners only
#
# Parameters:
# id (required) - The ID of a build
@@ -147,7 +159,9 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
authenticate_build_token!(build)
+
build.remove_artifacts_file!
+ build.remove_artifacts_metadata!
end
end
end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index e4ac0545ea2..b25e0e573a8 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -16,10 +16,19 @@ module Ci
end
class Build < Grape::Entity
- expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url,
- :before_sha, :allow_git_fetch, :project_name
-
+ expose :id, :ref, :tag, :sha, :status
expose :name, :token, :stage
+ expose :project_id
+ expose :project_name
+ expose :artifacts_file, using: ArtifactFile, if: lambda { |build, opts| build.artifacts? }
+ end
+
+ class BuildDetails < Build
+ expose :commands
+ expose :repo_url
+ expose :before_sha
+ expose :allow_git_fetch
+ expose :token
expose :options do |model|
model.options
@@ -30,7 +39,7 @@ module Ci
end
expose :variables
- expose :artifacts_file, using: ArtifactFile
+ expose :depends_on_builds, using: Build
end
class Runner < Grape::Entity
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 1c91204e98c..199d62d9b8a 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -34,10 +34,14 @@ module Ci
@runner ||= Runner.find_by_token(params[:token].to_s)
end
- def update_runner_info
+ def get_runner_version_from_params
return unless params["info"].present?
- info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
- current_runner.update(info)
+ attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
+ end
+
+ def update_runner_info
+ current_runner.assign_attributes(get_runner_version_from_params)
+ current_runner.save if current_runner.changed?
end
def max_artifacts_size
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index bfc14fe7a6b..192b1d18a51 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -47,6 +47,7 @@ module Ci
return forbidden! unless runner
if runner.id
+ runner.update(get_runner_version_from_params)
present runner, with: Entities::Runner
else
not_found!
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index bcdfd38d292..c89e1b51019 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -5,12 +5,14 @@ module Ci
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
- ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache]
+ ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
+ :allow_failure, :type, :stage, :when, :artifacts, :cache,
+ :dependencies]
attr_reader :before_script, :image, :services, :variables, :path, :cache
def initialize(config, path = nil)
- @config = YAML.safe_load(config, [Symbol])
+ @config = YAML.safe_load(config, [Symbol], [], true)
@path = path
unless @config.is_a? Hash
@@ -60,6 +62,7 @@ module Ci
@jobs = {}
@config.each do |key, job|
+ next if key.to_s.start_with?('.')
stage = job[:stage] || job[:type] || DEFAULT_STAGE
@jobs[key] = { stage: stage }.merge(job)
end
@@ -81,6 +84,7 @@ module Ci
services: job[:services] || @services,
artifacts: job[:artifacts],
cache: job[:cache] || @cache,
+ dependencies: job[:dependencies],
}.compact
}
end
@@ -115,6 +119,10 @@ module Ci
end
if @cache
+ if @cache[:key] && !validate_string(@cache[:key])
+ raise ValidationError, "cache:key parameter should be a string"
+ end
+
if @cache[:untracked] && !validate_boolean(@cache[:untracked])
raise ValidationError, "cache:untracked parameter should be an boolean"
end
@@ -139,6 +147,7 @@ module Ci
validate_job_stage!(name, job) if job[:stage]
validate_job_cache!(name, job) if job[:cache]
validate_job_artifacts!(name, job) if job[:artifacts]
+ validate_job_dependencies!(name, job) if job[:dependencies]
end
private
@@ -198,6 +207,10 @@ module Ci
end
def validate_job_cache!(name, job)
+ if job[:cache][:key] && !validate_string(job[:cache][:key])
+ raise ValidationError, "#{name} job: cache:key parameter should be a string"
+ end
+
if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked])
raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean"
end
@@ -208,6 +221,10 @@ module Ci
end
def validate_job_artifacts!(name, job)
+ if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
+ raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
+ end
+
if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
end
@@ -217,6 +234,22 @@ module Ci
end
end
+ def validate_job_dependencies!(name, job)
+ if !validate_array_of_strings(job[:dependencies])
+ raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
+ end
+
+ stage_index = stages.index(job[:stage])
+
+ job[:dependencies].each do |dependency|
+ raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]
+
+ unless stages.index(@jobs[dependency][:stage]) < stage_index
+ raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
+ end
+ end
+ end
+
def validate_array_of_strings(values)
values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
diff --git a/lib/ci/status.rb b/lib/ci/status.rb
index c02b3b8f3e4..3fb1fe29494 100644
--- a/lib/ci/status.rb
+++ b/lib/ci/status.rb
@@ -1,11 +1,9 @@
module Ci
class Status
def self.get_status(statuses)
- statuses.reject! { |status| status.try(&:allow_failure?) }
-
if statuses.none?
'skipped'
- elsif statuses.all?(&:success?)
+ elsif statuses.all? { |status| status.success? || status.ignored? }
'success'
elsif statuses.all?(&:pending?)
'pending'
diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb
new file mode 100644
index 00000000000..b366c89889e
--- /dev/null
+++ b/lib/gitlab/akismet_helper.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module AkismetHelper
+ def akismet_enabled?
+ current_application_settings.akismet_enabled
+ end
+
+ def akismet_client
+ @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
+ Gitlab.config.gitlab.url)
+ end
+
+ def check_for_spam?(project, user)
+ akismet_enabled? && !project.team.member?(user)
+ end
+
+ def is_spam?(environment, user, text)
+ client = akismet_client
+ ip_address = environment['REMOTE_ADDR']
+ user_agent = environment['HTTP_USER_AGENT']
+
+ params = {
+ type: 'comment',
+ text: text,
+ created_at: DateTime.now,
+ author: user.name,
+ author_email: user.email,
+ referrer: environment['HTTP_REFERER'],
+ }
+
+ begin
+ is_spam, is_blatant = client.check(ip_address, user_agent, params)
+ is_spam || is_blatant
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
+ false
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index b203b9d70e4..0b9c2e730f9 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -31,9 +31,7 @@ module Gitlab
html = ::Asciidoctor.convert(input, asciidoc_opts)
- if context[:project]
- html = Banzai.render(html, context.merge(pipeline: :asciidoc))
- end
+ html = Banzai.post_process(html, context)
html.html_safe
end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 459e3d6bcdb..b9bb6e76081 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -36,7 +36,7 @@ module Gitlab
# import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git")
#
def import_repository(name, url)
- output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '240'])
+ output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '900'])
raise Error, output unless status.zero?
true
end
@@ -47,7 +47,7 @@ module Gitlab
# new_path - new project path with namespace
#
# Ex.
- # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new.git")
+ # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new")
#
def mv_repository(path, new_path)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'mv-project',
@@ -150,6 +150,18 @@ module Gitlab
"#{path}.git", tag_name])
end
+ # Gc repository
+ #
+ # path - project path with namespace
+ #
+ # Ex.
+ # gc("gitlab/gitlab-ci")
+ #
+ def gc(path)
+ Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'gc',
+ "#{path}.git"])
+ end
+
# Add new key to gitlab-shell
#
# Ex.
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 2355b3c6ddc..46e51a4bf6d 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -13,12 +13,36 @@ module Gitlab
end
def execute
- project_identifier = project.import_source
+ import_issues if has_issues?
- return true unless client.project(project_identifier)["has_issues"]
+ true
+ rescue ActiveRecord::RecordInvalid => e
+ raise Projects::ImportService::Error.new, e.message
+ ensure
+ Gitlab::BitbucketImport::KeyDeleter.new(project).execute
+ end
- #Issues && Comments
- issues = client.issues(project_identifier)
+ private
+
+ def gl_user_id(project, bitbucket_id)
+ if bitbucket_id
+ user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
+ (user && user.id) || project.creator_id
+ else
+ project.creator_id
+ end
+ end
+
+ def identifier
+ project.import_source
+ end
+
+ def has_issues?
+ client.project(identifier)["has_issues"]
+ end
+
+ def import_issues
+ issues = client.issues(identifier)
issues.each do |issue|
body = ''
@@ -33,7 +57,7 @@ module Gitlab
body = @formatter.author_line(author)
body += issue["content"]
- comments = client.issue_comments(project_identifier, issue["local_id"])
+ comments = client.issue_comments(identifier, issue["local_id"])
if comments.any?
body += @formatter.comments_header
@@ -52,24 +76,13 @@ module Gitlab
project.issues.create!(
description: body,
title: issue["title"],
- state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened',
+ state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
author_id: gl_user_id(project, reporter)
)
end
-
- true
+ rescue ActiveRecord::RecordInvalid => e
+ raise Projects::ImportService::Error, e.message
end
-
- private
-
- def gl_user_id(project, bitbucket_id)
- if bitbucket_id
- user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
- (user && user.id) || project.creator_id
- else
- project.creator_id
- end
- end
end
end
end
diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb
new file mode 100644
index 00000000000..997a22779a0
--- /dev/null
+++ b/lib/gitlab/blame.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ class Blame
+ attr_accessor :blob, :commit
+
+ def initialize(blob, commit)
+ @blob = blob
+ @commit = commit
+ end
+
+ def groups(highlight: true)
+ prev_sha = nil
+ groups = []
+ current_group = nil
+
+ i = 0
+ blame.each do |commit, line|
+ commit = Commit.new(commit, project)
+
+ sha = commit.sha
+ if prev_sha != sha
+ groups << current_group if current_group
+ current_group = { commit: commit, lines: [] }
+ end
+
+ line = highlighted_lines[i].html_safe if highlight
+ current_group[:lines] << line
+
+ prev_sha = sha
+ i += 1
+ end
+ groups << current_group if current_group
+
+ groups
+ end
+
+ private
+
+ def blame
+ @blame ||= Gitlab::Git::Blame.new(repository, @commit.id, @blob.path)
+ end
+
+ def highlighted_lines
+ @blob.load_all_data!(repository)
+ @highlighted_lines ||= Gitlab::Highlight.highlight(@blob.name, @blob.data).lines
+ end
+
+ def project
+ commit.project
+ end
+
+ def repository
+ project.repository
+ end
+ end
+end
diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb
index 86bfa0a4378..34e949130da 100644
--- a/lib/gitlab/build_data_builder.rb
+++ b/lib/gitlab/build_data_builder.rb
@@ -23,6 +23,7 @@ module Gitlab
build_started_at: build.started_at,
build_finished_at: build.finished_at,
build_duration: build.duration,
+ build_allow_failure: build.allow_failure,
# TODO: do we still need it?
project_id: project.id,
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
new file mode 100644
index 00000000000..f2020c82d40
--- /dev/null
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -0,0 +1,111 @@
+require 'zlib'
+require 'json'
+
+module Gitlab
+ module Ci
+ module Build
+ module Artifacts
+ class Metadata
+ class ParserError < StandardError; end
+
+ VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
+ INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
+
+ attr_reader :file, :path, :full_version
+
+ def initialize(file, path, **opts)
+ @file, @path, @opts = file, path, opts
+ @full_version = read_version
+ end
+
+ def version
+ @full_version.match(VERSION_PATTERN)[1]
+ end
+
+ def errors
+ gzip do |gz|
+ read_string(gz) # version
+ errors = read_string(gz)
+ raise ParserError, 'Errors field not found!' unless errors
+
+ begin
+ JSON.parse(errors)
+ rescue JSON::ParserError
+ raise ParserError, 'Invalid errors field!'
+ end
+ end
+ end
+
+ def find_entries!
+ gzip do |gz|
+ 2.times { read_string(gz) } # version and errors fields
+ match_entries(gz)
+ end
+ end
+
+ def to_entry
+ entries = find_entries!
+ Entry.new(@path, entries)
+ end
+
+ private
+
+ def match_entries(gz)
+ entries = {}
+
+ child_pattern = '[^/]*/?$' unless @opts[:recursive]
+ match_pattern = /^#{Regexp.escape(@path)}#{child_pattern}/
+
+ until gz.eof? do
+ begin
+ path = read_string(gz).force_encoding('UTF-8')
+ meta = read_string(gz).force_encoding('UTF-8')
+
+ next unless path.valid_encoding? && meta.valid_encoding?
+ next unless path =~ match_pattern
+ next if path =~ INVALID_PATH_PATTERN
+
+ entries[path] = JSON.parse(meta, symbolize_names: true)
+ rescue JSON::ParserError, Encoding::CompatibilityError
+ next
+ end
+ end
+
+ entries
+ end
+
+ def read_version
+ gzip do |gz|
+ version_string = read_string(gz)
+
+ unless version_string
+ raise ParserError, 'Artifacts metadata file empty!'
+ end
+
+ unless version_string =~ VERSION_PATTERN
+ raise ParserError, 'Invalid version!'
+ end
+
+ version_string.chomp
+ end
+ end
+
+ def read_uint32(gz)
+ binary = gz.read(4)
+ binary.unpack('L>')[0] if binary
+ end
+
+ def read_string(gz)
+ string_size = read_uint32(gz)
+ return nil unless string_size
+ gz.read(string_size)
+ end
+
+ def gzip(&block)
+ Zlib::GzipReader.open(@file, &block)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
new file mode 100644
index 00000000000..7f4c750b6fd
--- /dev/null
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -0,0 +1,126 @@
+module Gitlab
+ module Ci::Build::Artifacts
+ class Metadata
+ ##
+ # Class that represents an entry (path and metadata) to a file or
+ # directory in GitLab CI Build Artifacts binary file / archive
+ #
+ # This is IO-operations safe class, that does similar job to
+ # Ruby's Pathname but without the risk of accessing filesystem.
+ #
+ # This class is working only with UTF-8 encoded paths.
+ #
+ class Entry
+ attr_reader :path, :entries
+ attr_accessor :name
+
+ def initialize(path, entries)
+ @path = path.dup.force_encoding('UTF-8')
+ @entries = entries
+
+ if path.include?("\0")
+ raise ArgumentError, 'Path contains zero byte character!'
+ end
+
+ unless path.valid_encoding?
+ raise ArgumentError, 'Path contains non-UTF-8 byte sequence!'
+ end
+ end
+
+ def directory?
+ blank_node? || @path.end_with?('/')
+ end
+
+ def file?
+ !directory?
+ end
+
+ def has_parent?
+ nodes > 0
+ end
+
+ def parent
+ return nil unless has_parent?
+ self.class.new(@path.chomp(basename), @entries)
+ end
+
+ def basename
+ (directory? && !blank_node?) ? name + '/' : name
+ end
+
+ def name
+ @name || @path.split('/').last.to_s
+ end
+
+ def children
+ return [] unless directory?
+ return @children if @children
+
+ child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$}
+ @children = select_entries { |path| path =~ child_pattern }
+ end
+
+ def directories(opts = {})
+ return [] unless directory?
+ dirs = children.select(&:directory?)
+ return dirs unless has_parent? && opts[:parent]
+
+ dotted_parent = parent
+ dotted_parent.name = '..'
+ dirs.prepend(dotted_parent)
+ end
+
+ def files
+ return [] unless directory?
+ children.select(&:file?)
+ end
+
+ def metadata
+ @entries[@path] || {}
+ end
+
+ def nodes
+ @path.count('/') + (file? ? 1 : 0)
+ end
+
+ def blank_node?
+ @path.empty? # "" is considered to be './'
+ end
+
+ def exists?
+ blank_node? || @entries.include?(@path)
+ end
+
+ def empty?
+ children.empty?
+ end
+
+ def total_size
+ descendant_pattern = %r{^#{Regexp.escape(@path)}}
+ entries.sum do |path, entry|
+ (entry[:size] if path =~ descendant_pattern).to_i
+ end
+ end
+
+ def to_s
+ @path
+ end
+
+ def ==(other)
+ @path == other.path && @entries == other.entries
+ end
+
+ def inspect
+ "#{self.class.name}: #{@path}"
+ end
+
+ private
+
+ def select_entries
+ selected = @entries.select { |path, _metadata| yield path }
+ selected.map { |path, _metadata| self.class.new(path, @entries) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/compare_result.rb b/lib/gitlab/compare_result.rb
deleted file mode 100644
index 0d696a1ee28..00000000000
--- a/lib/gitlab/compare_result.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Gitlab
- class CompareResult
- attr_reader :commits, :diffs
-
- def initialize(compare, diff_options = {})
- @commits, @diffs = compare.commits, compare.diffs(nil, diff_options)
- end
- end
-end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 8a7f8dc5003..85583dce9ee 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -45,11 +45,11 @@ module Gitlab
end
def starting_year
- (Time.now - 1.year).strftime("%Y")
+ 1.year.ago.year
end
def starting_month
- Date.today.strftime("%m").to_i
+ Date.today.month
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 7a86c09158e..761b63e98f6 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -4,11 +4,14 @@ module Gitlab
key = :current_application_settings
RequestStore.store[key] ||= begin
+ settings = nil
+
if connect_to_db?
- ApplicationSetting.current || ApplicationSetting.create_from_defaults
- else
- fake_application_settings
+ settings = ::ApplicationSetting.current
+ settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
end
+
+ settings || fake_application_settings
end
end
@@ -18,29 +21,36 @@ 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'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
- import_sources: Settings.gitlab['import_sources'],
+ default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
+ import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
+ require_two_factor_authentication: false,
+ two_factor_grace_period: 48,
+ akismet_enabled: false
)
end
private
def connect_to_db?
- use_db = if ENV['USE_DB'] == "false"
- false
- else
- true
- end
-
- use_db && ActiveRecord::Base.connection.active? &&
- !ActiveRecord::Migrator.needs_migration? &&
- ActiveRecord::Base.connection.table_exists?('application_settings')
+ # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
+ active_db_connection = ActiveRecord::Base.connection.active? rescue false
+
+ ENV['USE_DB'] != 'false' &&
+ active_db_connection &&
+ ActiveRecord::Base.connection.table_exists?('application_settings')
+
+ rescue ActiveRecord::NoDatabaseError
+ false
end
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index de77a6fbff1..6f9da69983a 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -1,16 +1,23 @@
module Gitlab
module Database
+ def self.adapter_name
+ connection.adapter_name
+ end
+
def self.mysql?
- ActiveRecord::Base.connection.adapter_name.downcase == 'mysql2'
+ adapter_name.downcase == 'mysql2'
end
def self.postgresql?
- ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
+ adapter_name.downcase == 'postgresql'
+ end
+
+ def self.version
+ database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
def true_value
- case ActiveRecord::Base.connection.adapter_name.downcase
- when 'postgresql'
+ if Gitlab::Database.postgresql?
"'t'"
else
1
@@ -18,12 +25,27 @@ module Gitlab
end
def false_value
- case ActiveRecord::Base.connection.adapter_name.downcase
- when 'postgresql'
+ if Gitlab::Database.postgresql?
"'f'"
else
0
end
end
+
+ private
+
+ def self.connection
+ ActiveRecord::Base.connection
+ end
+
+ def self.database_version
+ row = connection.execute("SELECT VERSION()").first
+
+ if postgresql?
+ row['version']
+ else
+ row.first
+ end
+ end
end
end
diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb
new file mode 100644
index 00000000000..a78fde9d782
--- /dev/null
+++ b/lib/gitlab/devise_failure.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ class DeviseFailure < Devise::FailureApp
+ protected
+
+ # Override `Devise::FailureApp#request_format` to handle a special case
+ #
+ # This tells Devise to handle an unauthenticated `.zip` request as an HTML
+ # request (i.e., redirect to sign in).
+ #
+ # Otherwise, Devise would respond with a 401 Unauthorized with
+ # `Content-Type: application/zip` and a response body in plaintext, and the
+ # browser would freak out.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/12944
+ def request_format
+ if request.format == :zip
+ Mime::Type.lookup_by_extension(:html).ref
+ else
+ super
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 79061cd0141..d2e85cabf72 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -1,18 +1,39 @@
module Gitlab
module Diff
class File
- attr_reader :diff
+ attr_reader :diff, :diff_refs
delegate :new_file, :deleted_file, :renamed_file,
:old_path, :new_path, to: :diff, prefix: false
- def initialize(diff)
+ def initialize(diff, diff_refs)
@diff = diff
+ @diff_refs = diff_refs
+ end
+
+ def old_ref
+ diff_refs[0] if diff_refs
+ end
+
+ def new_ref
+ diff_refs[1] if diff_refs
end
# Array of Gitlab::DIff::Line objects
def diff_lines
- @lines ||= parser.parse(raw_diff.lines)
+ @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
+
+ def parallel_diff_lines
+ Gitlab::Diff::ParallelDiff.new(self).parallelize
end
def mode_changed?
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
new file mode 100644
index 00000000000..9429b3ff88d
--- /dev/null
+++ b/lib/gitlab/diff/highlight.rb
@@ -0,0 +1,77 @@
+module Gitlab
+ module Diff
+ class Highlight
+ attr_reader :diff_file, :diff_lines, :raw_lines
+
+ delegate :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff
+
+ def initialize(diff_lines)
+ if diff_lines.is_a?(Gitlab::Diff::File)
+ @diff_file = diff_lines
+ @diff_lines = @diff_file.diff_lines
+ else
+ @diff_lines = diff_lines
+ end
+ @raw_lines = @diff_lines.map(&:text)
+ end
+
+ def highlight
+ @diff_lines.map.with_index do |diff_line, i|
+ diff_line = diff_line.dup
+ # ignore highlighting for "match" lines
+ next diff_line if diff_line.type == 'match' || diff_line.type == 'nonewline'
+
+ rich_line = highlight_line(diff_line) || diff_line.text
+
+ if line_inline_diffs = inline_diffs[i]
+ rich_line = InlineDiffMarker.new(diff_line.text, rich_line).mark(line_inline_diffs)
+ end
+
+ diff_line.text = rich_line
+
+ diff_line
+ end
+ end
+
+ private
+
+ def highlight_line(diff_line)
+ return unless diff_file && diff_file.diff_refs
+
+ line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
+
+ case diff_line.type
+ when 'new', nil
+ rich_line = new_lines[diff_line.new_pos - 1]
+ when 'old'
+ rich_line = old_lines[diff_line.old_pos - 1]
+ end
+
+ # Only update text if line is found. This will prevent
+ # issues with submodules given the line only exists in diff content.
+ "#{line_prefix}#{rich_line}".html_safe if rich_line
+ end
+
+ def inline_diffs
+ @inline_diffs ||= InlineDiff.for_lines(@raw_lines)
+ end
+
+ def old_lines
+ return unless diff_file
+ @old_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:old))
+ end
+
+ def new_lines
+ return unless diff_file
+ @new_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:new))
+ end
+
+ def processing_args(version)
+ ref = send("diff_#{version}_ref")
+ path = send("diff_#{version}_path")
+
+ [ref.project.repository, ref.id, path]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
new file mode 100644
index 00000000000..789c14518b0
--- /dev/null
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -0,0 +1,88 @@
+module Gitlab
+ module Diff
+ class InlineDiff
+ attr_accessor :old_line, :new_line, :offset
+
+ def self.for_lines(lines)
+ local_edit_indexes = self.find_local_edits(lines)
+
+ inline_diffs = []
+
+ local_edit_indexes.each do |index|
+ old_index = index
+ new_index = index + 1
+ old_line = lines[old_index]
+ new_line = lines[new_index]
+
+ old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs
+
+ inline_diffs[old_index] = old_diffs
+ inline_diffs[new_index] = new_diffs
+ end
+
+ inline_diffs
+ end
+
+ def initialize(old_line, new_line, offset: 0)
+ @old_line = old_line[offset..-1]
+ @new_line = new_line[offset..-1]
+ @offset = offset
+ end
+
+ def inline_diffs
+ # Skip inline diff if empty line was replaced with content
+ return if old_line == ""
+
+ lcp = longest_common_prefix(old_line, new_line)
+ lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1])
+
+ lcp += offset
+ old_length = old_line.length + offset
+ new_length = new_line.length + offset
+
+ old_diff_range = lcp..(old_length - lcs - 1)
+ new_diff_range = lcp..(new_length - lcs - 1)
+
+ old_diffs = [old_diff_range] if old_diff_range.begin <= old_diff_range.end
+ new_diffs = [new_diff_range] if new_diff_range.begin <= new_diff_range.end
+
+ [old_diffs, new_diffs]
+ end
+
+ private
+
+ def self.find_local_edits(lines)
+ line_prefixes = lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' }
+ joined_line_prefixes = " #{line_prefixes.join} "
+
+ offset = 0
+ local_edit_indexes = []
+ while index = joined_line_prefixes.index(" -+ ", offset)
+ local_edit_indexes << index
+ offset = index + 1
+ end
+
+ local_edit_indexes
+ end
+
+ def longest_common_prefix(a, b)
+ max_length = [a.length, b.length].max
+
+ length = 0
+ (0..max_length - 1).each do |pos|
+ old_char = a[pos]
+ new_char = b[pos]
+
+ break if old_char != new_char
+ length += 1
+ end
+
+ length
+ end
+
+ def longest_common_suffix(a, b)
+ longest_common_prefix(a.reverse, b.reverse)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
new file mode 100644
index 00000000000..dccb717e95d
--- /dev/null
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -0,0 +1,115 @@
+module Gitlab
+ module Diff
+ class InlineDiffMarker
+ attr_accessor :raw_line, :rich_line
+
+ def initialize(raw_line, rich_line = raw_line)
+ @raw_line = raw_line
+ @rich_line = ERB::Util.html_escape(rich_line)
+ end
+
+ def mark(line_inline_diffs)
+ return rich_line unless line_inline_diffs
+
+ marker_ranges = []
+ line_inline_diffs.each do |inline_diff_range|
+ # Map the inline-diff range based on the raw line to character positions in the rich line
+ inline_diff_positions = position_mapping[inline_diff_range].flatten
+ # Turn the array of character positions into ranges
+ marker_ranges.concat(collapse_ranges(inline_diff_positions))
+ end
+
+ offset = 0
+ # Mark each range
+ marker_ranges.each_with_index do |range, i|
+ class_names = ["idiff"]
+ class_names << "left" if i == 0
+ class_names << "right" if i == marker_ranges.length - 1
+
+ offset = insert_around_range(rich_line, range, "<span class='#{class_names.join(" ")}'>", "</span>", offset)
+ end
+
+ rich_line.html_safe
+ end
+
+ private
+
+ # Mapping of character positions in the raw line, to the rich (highlighted) line
+ def position_mapping
+ @position_mapping ||= begin
+ mapping = []
+ rich_pos = 0
+ (0..raw_line.length).each do |raw_pos|
+ rich_char = rich_line[rich_pos]
+
+ # The raw and rich lines are the same except for HTML tags,
+ # so skip over any `<...>` segment
+ while rich_char == '<'
+ until rich_char == '>'
+ rich_pos += 1
+ rich_char = rich_line[rich_pos]
+ end
+
+ rich_pos += 1
+ rich_char = rich_line[rich_pos]
+ end
+
+ # multi-char HTML entities in the rich line correspond to a single character in the raw line
+ if rich_char == '&'
+ multichar_mapping = [rich_pos]
+ until rich_char == ';'
+ rich_pos += 1
+ multichar_mapping << rich_pos
+ rich_char = rich_line[rich_pos]
+ end
+
+ mapping[raw_pos] = multichar_mapping
+ else
+ mapping[raw_pos] = rich_pos
+ end
+
+ rich_pos += 1
+ end
+
+ mapping
+ end
+ end
+
+ # Takes an array of integers, and returns an array of ranges covering the same integers
+ def collapse_ranges(positions)
+ return [] if positions.empty?
+ ranges = []
+
+ start = prev = positions[0]
+ range = start..prev
+ positions[1..-1].each do |pos|
+ if pos == prev + 1
+ range = start..pos
+ prev = pos
+ else
+ ranges << range
+ start = prev = pos
+ range = start..prev
+ end
+ end
+ ranges << range
+
+ ranges
+ end
+
+ # Inserts tags around the characters identified by the given range
+ def insert_around_range(text, range, before, after, offset = 0)
+ # Just to be sure
+ return offset if offset + range.end + 1 > text.length
+
+ text.insert(offset + range.begin, before)
+ offset += before.length
+
+ text.insert(offset + range.end + 1, after)
+ offset += after.length
+
+ offset
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 0072194606e..03730b435ad 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -1,7 +1,8 @@
module Gitlab
module Diff
class Line
- attr_reader :type, :text, :index, :old_pos, :new_pos
+ attr_reader :type, :index, :old_pos, :new_pos
+ attr_accessor :text
def initialize(text, type, index, old_pos, new_pos)
@text, @type, @index = text, type, index
diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb
new file mode 100644
index 00000000000..74f9b3c050a
--- /dev/null
+++ b/lib/gitlab/diff/parallel_diff.rb
@@ -0,0 +1,119 @@
+module Gitlab
+ module Diff
+ class ParallelDiff
+ attr_accessor :diff_file
+
+ def initialize(diff_file)
+ @diff_file = diff_file
+ end
+
+ def parallelize
+ lines = []
+ skip_next = false
+
+ highlighted_diff_lines = diff_file.highlighted_diff_lines
+ highlighted_diff_lines.each do |line|
+ full_line = line.text
+ type = line.type
+ line_code = generate_line_code(diff_file.file_path, line)
+ line_new = line.new_pos
+ line_old = line.old_pos
+
+ next_line = diff_file.next_line(line.index)
+
+ if next_line
+ next_line = highlighted_diff_lines[next_line.index]
+ next_line_code = generate_line_code(diff_file.file_path, next_line)
+ next_type = next_line.type
+ next_line = next_line.text
+ end
+
+ case type
+ when 'match', nil
+ # line in the right panel is the same as in the left one
+ lines << {
+ left: {
+ type: type,
+ number: line_old,
+ text: full_line,
+ line_code: line_code,
+ },
+ right: {
+ type: type,
+ number: line_new,
+ text: full_line,
+ line_code: line_code
+ }
+ }
+ when 'old'
+ case next_type
+ when 'new'
+ # Left side has text removed, right side has text added
+ lines << {
+ left: {
+ type: type,
+ number: line_old,
+ text: full_line,
+ line_code: line_code,
+ },
+ right: {
+ type: next_type,
+ number: line_new,
+ text: next_line,
+ line_code: next_line_code
+ }
+ }
+ skip_next = true
+ when 'old', 'nonewline', nil
+ # Left side has text removed, right side doesn't have any change
+ # No next line code, no new line number, no new line text
+ lines << {
+ left: {
+ type: type,
+ number: line_old,
+ text: full_line,
+ line_code: line_code,
+ },
+ right: {
+ type: next_type,
+ number: nil,
+ text: "",
+ line_code: nil
+ }
+ }
+ end
+ when 'new'
+ if skip_next
+ # Change has been already included in previous line so no need to do it again
+ skip_next = false
+ next
+ else
+ # Change is only on the right side, left side has no change
+ lines << {
+ left: {
+ type: nil,
+ number: nil,
+ text: "",
+ line_code: line_code,
+ },
+ right: {
+ type: type,
+ number: line_new,
+ text: full_line,
+ line_code: line_code
+ }
+ }
+ end
+ end
+ end
+ lines
+ end
+
+ private
+
+ def generate_line_code(file_path, line)
+ Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 7015fe36c3d..d0815fc7eea 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -4,49 +4,56 @@ module Gitlab
include Enumerable
def parse(lines)
+ return [] if lines.blank?
+
@lines = lines
- lines_obj = []
line_obj_index = 0
line_old = 1
line_new = 1
type = nil
- lines_arr = ::Gitlab::InlineDiff.processing lines
-
- lines_arr.each do |line|
- next if filename?(line)
-
- full_line = html_escape(line.gsub(/\n/, ''))
- full_line = ::Gitlab::InlineDiff.replace_markers full_line
-
- if line.match(/^@@ -/)
- type = "match"
-
- line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0
- line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0
-
- next if line_old <= 1 && line_new <= 1 #top of file
- lines_obj << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
- line_obj_index += 1
- next
- else
- type = identification_type(line)
- lines_obj << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
- line_obj_index += 1
- end
-
-
- if line[0] == "+"
- line_new += 1
- elsif line[0] == "-"
- line_old += 1
- else
- line_new += 1
- line_old += 1
+ # By returning an Enumerator we make it possible to search for a single line (with #find)
+ # without having to instantiate all the others that come after it.
+ Enumerator.new do |yielder|
+ @lines.each do |line|
+ next if filename?(line)
+
+ full_line = line.gsub(/\n/, '')
+
+ if line.match(/^@@ -/)
+ type = "match"
+
+ line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0
+ line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0
+
+ next if line_old <= 1 && line_new <= 1 #top of file
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ line_obj_index += 1
+ next
+ elsif line[0] == '\\'
+ type = 'nonewline'
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ line_obj_index += 1
+ else
+ type = identification_type(line)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ line_obj_index += 1
+ end
+
+
+ case line[0]
+ when "+"
+ line_new += 1
+ when "-"
+ line_old += 1
+ when "\\"
+ # No increment
+ else
+ line_new += 1
+ line_old += 1
+ end
end
end
-
- lines_obj
end
def empty?
@@ -56,24 +63,21 @@ module Gitlab
private
def filename?(line)
- line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
- '--- /tmp/diffy', '+++ /tmp/diffy')
+ line.start_with?( '--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
+ '+++ a', # The line will start with `+++ a` in the reverse diff of an orphan commit
+ '--- /tmp/diffy', '+++ /tmp/diffy')
end
def identification_type(line)
- if line[0] == "+"
+ case line[0]
+ when "+"
"new"
- elsif line[0] == "-"
+ when "-"
"old"
else
nil
end
end
-
- def html_escape(str)
- replacements = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
- str.gsub(/[&"'><]/, replacements)
- end
end
end
end
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index a2eb7a70bd2..41f0edcaf7e 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -9,6 +9,7 @@ module Gitlab
delegate :namespace, :name_with_namespace, to: :project, prefix: :project
delegate :name, to: :author, prefix: :author
+ delegate :username, to: :author, prefix: :author
def initialize(notify, project_id, recipient, opts = {})
raise ArgumentError, 'Missing options: author_id, ref, action' unless
@@ -49,7 +50,7 @@ module Gitlab
end
def compare_timeout
- compare.timeout if compare
+ diffs.overflow? if diffs
end
def reverse_compare?
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 2b252b32887..2ca21af5bc8 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -74,7 +74,7 @@ module Gitlab
def sent_notification
return nil unless reply_key
-
+
SentNotification.for(reply_key)
end
@@ -82,10 +82,7 @@ module Gitlab
attachments = Email::AttachmentUploader.new(message).execute(sent_notification.project)
attachments.each do |link|
- text = "[#{link[:alt]}](#{link[:url]})"
- text.prepend("!") if link[:is_image]
-
- reply << "\n\n#{text}"
+ reply << "\n\n#{link[:markdown]}"
end
reply
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
new file mode 100644
index 00000000000..2ef50286b1d
--- /dev/null
+++ b/lib/gitlab/exclusive_lease.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ # This class implements an 'exclusive lease'. We call it a 'lease'
+ # because it has a set expiry time. We call it 'exclusive' because only
+ # one caller may obtain a lease for a given key at a time. The
+ # implementation is intended to work across GitLab processes and across
+ # servers. It is a 'cheap' alternative to using SQL queries and updates:
+ # you do not need to change the SQL schema to start using
+ # ExclusiveLease.
+ #
+ # It is important to choose the timeout wisely. If the timeout is very
+ # high (1 hour) then the throughput of your operation gets very low (at
+ # most once an hour). If the timeout is lower than how long your
+ # operation may take then you cannot count on exclusivity. For example,
+ # if the timeout is 10 seconds and you do an operation which may take 20
+ # seconds then two overlapping operations may hold a lease for the same
+ # key at the same time.
+ #
+ class ExclusiveLease
+ def initialize(key, timeout:)
+ @key, @timeout = key, timeout
+ end
+
+ # Try to obtain the lease. Return true on success,
+ # false if the lease is already taken.
+ def try_obtain
+ # Performing a single SET is atomic
+ !!redis.set(redis_key, '1', nx: true, ex: @timeout)
+ end
+
+ private
+
+ def redis
+ # Maybe someday we want to use a connection pool...
+ @redis ||= Redis.new(url: Gitlab::RedisConfig.url)
+ end
+
+ def redis_key
+ "gitlab:exclusive_lease:#{@key}"
+ end
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 403ebeec474..db580b5e578 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -232,9 +232,7 @@ module Gitlab
return nil if res.nil?
- text = "[#{res['alt']}](#{res['url']})"
- text = "!#{text}" if res['is_image']
- text
+ res[:markdown]
end
def build_attachment_url(rel_url)
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index f065cc5e9e9..191bea86ac3 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -1,8 +1,8 @@
module Gitlab
module Git
- BLANK_SHA = '0' * 40
- TAG_REF_PREFIX = "refs/tags/"
- BRANCH_REF_PREFIX = "refs/heads/"
+ BLANK_SHA = ('0' * 40).freeze
+ TAG_REF_PREFIX = "refs/tags/".freeze
+ BRANCH_REF_PREFIX = "refs/heads/".freeze
class << self
def ref_name(ref)
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
new file mode 100644
index 00000000000..a088e19d1e7
--- /dev/null
+++ b/lib/gitlab/git_post_receive.rb
@@ -0,0 +1,60 @@
+module Gitlab
+ class GitPostReceive
+ include Gitlab::Identifier
+ attr_reader :repo_path, :identifier, :changes, :project
+
+ def initialize(repo_path, identifier, changes)
+ repo_path.gsub!(/\.git\z/, '')
+ repo_path.gsub!(/\A\//, '')
+
+ @repo_path = repo_path
+ @identifier = identifier
+ @changes = deserialize_changes(changes)
+
+ retrieve_project_and_type
+ end
+
+ def wiki?
+ @type == :wiki
+ end
+
+ def regular_project?
+ @type == :project
+ end
+
+ def identify(revision)
+ super(identifier, project, revision)
+ end
+
+ private
+
+ def retrieve_project_and_type
+ @type = :project
+ @project = Project.find_with_namespace(@repo_path)
+
+ if @repo_path.end_with?('.wiki') && !@project
+ @type = :wiki
+ @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, ''))
+ end
+ end
+
+ def deserialize_changes(changes)
+ changes = Base64.decode64(changes) unless changes.include?(' ')
+ changes = utf8_encode_changes(changes)
+ changes.lines
+ end
+
+ def utf8_encode_changes(changes)
+ changes = changes.dup
+
+ changes.force_encoding('UTF-8')
+ return changes if changes.valid_encoding?
+
+ # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON.
+ detection = CharlockHolmes::EncodingDetector.detect(changes)
+ return changes unless detection && detection[:encoding]
+
+ CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8')
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
new file mode 100644
index 00000000000..202263c6742
--- /dev/null
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module GithubImport
+ class BaseFormatter
+ attr_reader :formatter, :project, :raw_data
+
+ def initialize(project, raw_data)
+ @project = project
+ @raw_data = raw_data
+ @formatter = Gitlab::ImportFormatter.new
+ end
+
+ private
+
+ def gl_user_id(github_id)
+ User.joins(:identities).
+ find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
+ try(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
new file mode 100644
index 00000000000..7d58e53991a
--- /dev/null
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module GithubImport
+ class CommentFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ note: note,
+ commit_id: raw_data.commit_id,
+ line_code: line_code,
+ author_id: author_id,
+ created_at: raw_data.created_at,
+ updated_at: raw_data.updated_at
+ }
+ end
+
+ private
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gl_user_id(raw_data.user.id) || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def line_code
+ if on_diff?
+ Gitlab::Diff::LineCode.generate(raw_data.path, raw_data.position, 0)
+ end
+ end
+
+ def on_diff?
+ raw_data.path && raw_data.position
+ end
+
+ def note
+ formatter.author_line(author) + body
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index b5720f6e2cb..172c5441e36 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class Importer
+ include Gitlab::ShellAdapter
+
attr_reader :project, :client
def initialize(project)
@@ -12,39 +14,86 @@ module Gitlab
end
def execute
- #Issues && Comments
+ import_issues && import_pull_requests && import_wiki
+ end
+
+ private
+
+ def import_issues
client.list_issues(project.import_source, state: :all,
sort: :created,
- direction: :asc).each do |issue|
- if issue.pull_request.nil?
-
- body = @formatter.author_line(issue.user.login)
- body += issue.body || ""
+ direction: :asc).each do |raw_data|
+ gh_issue = IssueFormatter.new(project, raw_data)
- if issue.comments > 0
- body += @formatter.comments_header
+ if gh_issue.valid?
+ issue = Issue.create!(gh_issue.attributes)
- client.issue_comments(project.import_source, issue.number).each do |c|
- body += @formatter.comment(c.user.login, c.created_at, c.body)
- end
+ if gh_issue.has_comments?
+ import_comments(gh_issue.number, issue)
end
+ end
+ end
+
+ true
+ rescue ActiveRecord::RecordInvalid => e
+ raise Projects::ImportService::Error, e.message
+ end
+
+ def import_pull_requests
+ client.pull_requests(project.import_source, state: :all,
+ sort: :created,
+ direction: :asc).each do |raw_data|
+ pull_request = PullRequestFormatter.new(project, raw_data)
+
+ if pull_request.valid?
+ merge_request = MergeRequest.new(pull_request.attributes)
- project.issues.create!(
- description: body,
- title: issue.title,
- state: issue.state == 'closed' ? 'closed' : 'opened',
- author_id: gl_user_id(project, issue.user.id)
- )
+ if merge_request.save
+ import_comments(pull_request.number, merge_request)
+ import_comments_on_diff(pull_request.number, merge_request)
+ end
end
end
+
+ true
+ rescue ActiveRecord::RecordInvalid => e
+ raise Projects::ImportService::Error, e.message
end
- private
+ def import_comments(issue_number, noteable)
+ comments = client.issue_comments(project.import_source, issue_number)
+ create_comments(comments, noteable)
+ end
+
+ def import_comments_on_diff(pull_request_number, merge_request)
+ comments = client.pull_request_comments(project.import_source, pull_request_number)
+ create_comments(comments, merge_request)
+ end
- def gl_user_id(project, github_id)
- user = User.joins(:identities).
- find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s)
- (user && user.id) || project.creator_id
+ def create_comments(comments, noteable)
+ comments.each do |raw_data|
+ comment = CommentFormatter.new(project, raw_data)
+ noteable.notes.create!(comment.attributes)
+ end
+ end
+
+ def import_wiki
+ unless project.wiki_enabled?
+ wiki = WikiFormatter.new(project)
+ gitlab_shell.import_repository(wiki.path_with_namespace, wiki.import_url)
+ project.update_attribute(:wiki_enabled, true)
+ end
+
+ true
+ rescue Gitlab::Shell::Error => e
+ # GitHub error message when the wiki repo has not been created,
+ # this means that repo has wiki enabled, but have no pages. So,
+ # we can skip the import.
+ if e.message !~ /repository not exported/
+ raise Projects::ImportService::Error, e.message
+ else
+ true
+ end
end
end
end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
new file mode 100644
index 00000000000..1e3ba44f27c
--- /dev/null
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -0,0 +1,66 @@
+module Gitlab
+ module GithubImport
+ class IssueFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ title: raw_data.title,
+ description: description,
+ state: state,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ created_at: raw_data.created_at,
+ updated_at: updated_at
+ }
+ end
+
+ def has_comments?
+ raw_data.comments > 0
+ end
+
+ def number
+ raw_data.number
+ end
+
+ def valid?
+ raw_data.pull_request.nil?
+ end
+
+ private
+
+ def assigned?
+ raw_data.assignee.present?
+ end
+
+ def assignee_id
+ if assigned?
+ gl_user_id(raw_data.assignee.id)
+ end
+ end
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gl_user_id(raw_data.user.id) || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def description
+ @formatter.author_line(author) + body
+ end
+
+ def state
+ raw_data.state == 'closed' ? 'closed' : 'opened'
+ end
+
+ def updated_at
+ state == 'closed' ? raw_data.closed_at : raw_data.updated_at
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 8c27ebd1ce8..474927069a5 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -20,7 +20,8 @@ module Gitlab
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
import_type: "github",
import_source: repo.full_name,
- import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
+ import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"),
+ wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later
).execute
project.create_import_data(data: { "github_session" => session_data } )
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
new file mode 100644
index 00000000000..4e507b090e8
--- /dev/null
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -0,0 +1,105 @@
+module Gitlab
+ module GithubImport
+ class PullRequestFormatter < BaseFormatter
+ def attributes
+ {
+ title: raw_data.title,
+ description: description,
+ source_project: source_project,
+ source_branch: source_branch.name,
+ target_project: target_project,
+ target_branch: target_branch.name,
+ state: state,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ created_at: raw_data.created_at,
+ updated_at: updated_at
+ }
+ end
+
+ def number
+ raw_data.number
+ end
+
+ def valid?
+ !cross_project? && source_branch.present? && target_branch.present?
+ end
+
+ private
+
+ def assigned?
+ raw_data.assignee.present?
+ end
+
+ def assignee_id
+ if assigned?
+ gl_user_id(raw_data.assignee.id)
+ end
+ end
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gl_user_id(raw_data.user.id) || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def cross_project?
+ source_repo.present? && target_repo.present? && source_repo.id != target_repo.id
+ end
+
+ def description
+ formatter.author_line(author) + body
+ end
+
+ def source_project
+ project
+ end
+
+ def source_repo
+ raw_data.head.repo
+ end
+
+ def source_branch
+ source_project.repository.find_branch(raw_data.head.ref)
+ end
+
+ def target_project
+ project
+ end
+
+ def target_repo
+ raw_data.base.repo
+ end
+
+ def target_branch
+ target_project.repository.find_branch(raw_data.base.ref)
+ end
+
+ def state
+ @state ||= case true
+ when raw_data.state == 'closed' && raw_data.merged_at.present?
+ 'merged'
+ when raw_data.state == 'closed'
+ 'closed'
+ else
+ 'opened'
+ end
+ end
+
+ def updated_at
+ case state
+ when 'merged' then raw_data.merged_at
+ when 'closed' then raw_data.closed_at
+ else
+ raw_data.updated_at
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/wiki_formatter.rb b/lib/gitlab/github_import/wiki_formatter.rb
new file mode 100644
index 00000000000..6c592ff469c
--- /dev/null
+++ b/lib/gitlab/github_import/wiki_formatter.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module GithubImport
+ class WikiFormatter
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def path_with_namespace
+ "#{project.path_with_namespace}.wiki"
+ end
+
+ def import_url
+ project.import_url.sub(/\.git\z/, ".wiki.git")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index e24b94d6159..850b73244c6 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def execute
- project_identifier = URI.encode(project.import_source, '/')
+ project_identifier = CGI.escape(project.import_source)
#Issues && Comments
issues = client.issues(project_identifier)
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
new file mode 100644
index 00000000000..cac76442321
--- /dev/null
+++ b/lib/gitlab/highlight.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ class Highlight
+ def self.highlight(blob_name, blob_content, nowrap: true)
+ new(blob_name, blob_content, nowrap: nowrap).highlight(blob_content, continue: false)
+ end
+
+ def self.highlight_lines(repository, ref, file_name)
+ blob = repository.blob_at(ref, file_name)
+ return [] unless blob
+
+ blob.load_all_data!(repository)
+ highlight(file_name, blob.data).lines.map!(&:html_safe)
+ end
+
+ def initialize(blob_name, blob_content, nowrap: true)
+ @formatter = rouge_formatter(nowrap: nowrap)
+ @lexer = Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText
+ end
+
+ def highlight(text, continue: true)
+ @formatter.format(@lexer.lex(text, continue: continue)).html_safe
+ rescue
+ @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
+ end
+
+ private
+
+ def rouge_formatter(options = {})
+ options = options.reverse_merge(
+ nowrap: true,
+ cssclass: 'code highlight',
+ lineanchors: true,
+ lineanchorsid: 'LC'
+ )
+
+ Rouge::Formatters::HTMLGitlab.new(options)
+ end
+ end
+end
diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb
deleted file mode 100644
index 44507bde25d..00000000000
--- a/lib/gitlab/inline_diff.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-module Gitlab
- class InlineDiff
- class << self
-
- START = "#!idiff-start!#"
- FINISH = "#!idiff-finish!#"
-
- def processing(diff_arr)
- indexes = _indexes_of_changed_lines diff_arr
-
- indexes.each do |index|
- first_line = diff_arr[index+1]
- second_line = diff_arr[index+2]
-
- # Skip inline diff if empty line was replaced with content
- next if first_line == "-\n"
-
- first_token = find_first_token(first_line, second_line)
- apply_first_token(diff_arr, index, first_token)
-
- last_token = find_last_token(first_line, second_line, first_token)
- apply_last_token(diff_arr, index, last_token)
- end
-
- diff_arr
- end
-
- def apply_first_token(diff_arr, index, first_token)
- start = first_token + START
-
- if first_token.empty?
- # In case if we remove string of spaces in commit
- diff_arr[index+1].sub!("-", "-" => "-#{START}")
- diff_arr[index+2].sub!("+", "+" => "+#{START}")
- else
- diff_arr[index+1].sub!(first_token, first_token => start)
- diff_arr[index+2].sub!(first_token, first_token => start)
- end
- end
-
- def apply_last_token(diff_arr, index, last_token)
- # This is tricky: escape backslashes so that `sub` doesn't interpret them
- # as backreferences. Regexp.escape does NOT do the right thing.
- replace_token = FINISH + last_token.gsub(/\\/, '\&\&')
- diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
- diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
- end
-
- def find_first_token(first_line, second_line)
- max_length = [first_line.size, second_line.size].max
- first_the_same_symbols = 0
-
- (0..max_length + 1).each do |i|
- first_the_same_symbols = i - 1
-
- if first_line[i] != second_line[i] && i > 0
- break
- end
- end
-
- first_line[0..first_the_same_symbols][1..-1]
- end
-
- def find_last_token(first_line, second_line, first_token)
- max_length = [first_line.size, second_line.size].max
- last_the_same_symbols = 0
-
- (1..max_length + 1).each do |i|
- last_the_same_symbols = -i
- shortest_line = second_line.size > first_line.size ? first_line : second_line
-
- if (first_line[-i] != second_line[-i]) || "#{first_token}#{START}".size == shortest_line[1..-i].size
- break
- end
- end
-
- last_the_same_symbols += 1
- first_line[last_the_same_symbols..-1]
- end
-
- def _indexes_of_changed_lines(diff_arr)
- chain_of_first_symbols = ""
- diff_arr.each_with_index do |line, i|
- chain_of_first_symbols += line[0]
- end
- chain_of_first_symbols.gsub!(/[^\-\+]/, "#")
-
- offset = 0
- indexes = []
- while index = chain_of_first_symbols.index("#-+#", offset)
- indexes << index
- offset = index + 1
- end
- indexes
- end
-
- def replace_markers(line)
- line.gsub!(START, "<span class='idiff'>")
- line.gsub!(FINISH, "</span>")
- line
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index c438a3d167b..da4435c7308 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -5,7 +5,7 @@
module Gitlab
module LDAP
class Access
- attr_reader :adapter, :provider, :user
+ attr_reader :provider, :user
def self.open(user, &block)
Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
@@ -32,20 +32,20 @@ module Gitlab
end
def allowed?
- if Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ if ldap_user
return true unless ldap_config.active_directory
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
- user.block
+ user.ldap_block
false
else
- user.activate if user.blocked? && !ldap_config.block_auto_created_users
+ user.activate if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
- user.block
+ user.ldap_block
false
end
rescue
@@ -59,6 +59,10 @@ module Gitlab
def ldap_config
Gitlab::LDAP::Config.new(provider)
end
+
+ def ldap_user
+ @ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ end
end
end
end
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 577a890a7d9..df65179bfea 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -70,19 +70,25 @@ module Gitlab
end
def ldap_search(*args)
- results = ldap.search(*args)
+ # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
+ Timeout.timeout(config.timeout) do
+ results = ldap.search(*args)
- if results.nil?
- response = ldap.get_operation_result
+ if results.nil?
+ response = ldap.get_operation_result
- unless response.code.zero?
- Rails.logger.warn("LDAP search error: #{response.message}")
- end
+ unless response.code.zero?
+ Rails.logger.warn("LDAP search error: #{response.message}")
+ end
- []
- else
- results
+ []
+ else
+ results
+ end
end
+ rescue Timeout::Error
+ Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
+ []
end
end
end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index 101a3285f4b..aff7ccb157f 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -88,6 +88,10 @@ module Gitlab
options['attributes']
end
+ def timeout
+ options['timeout'].to_i
+ end
+
protected
def base_config
Gitlab.config.ldap
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index aef08c97d1d..b84c81f1a6c 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -24,34 +24,41 @@ module Gitlab
update_user_attributes
end
+ def save
+ super('LDAP')
+ end
+
# instance methods
def gl_user
@gl_user ||= find_by_uid_and_provider || find_by_email || build_new_user
end
def find_by_uid_and_provider
- self.class.find_by_uid_and_provider(
- auth_hash.uid, auth_hash.provider)
+ self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end
def find_by_email
- ::User.find_by(email: auth_hash.email.downcase)
+ ::User.find_by(email: auth_hash.email.downcase) if auth_hash.has_email?
end
def update_user_attributes
- return unless persisted?
+ if persisted?
+ if auth_hash.has_email?
+ gl_user.skip_reconfirmation!
+ gl_user.email = auth_hash.email
+ end
- gl_user.skip_reconfirmation!
- gl_user.email = auth_hash.email
+ # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
+ identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
+ identity ||= gl_user.identities.build(provider: auth_hash.provider)
- # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
- identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
- identity ||= gl_user.identities.build(provider: auth_hash.provider)
+ # For a new identity set extern_uid to the LDAP DN
+ # For an existing identity with matching email but changed DN, update the DN.
+ # For an existing identity with no change in DN, this line changes nothing.
+ identity.extern_uid = auth_hash.uid
+ end
- # For a new user set extern_uid to the LDAP DN
- # For an existing user with matching email but changed DN, update the DN.
- # For an existing user with no change in DN, this line changes nothing.
- identity.extern_uid = auth_hash.uid
+ gl_user.ldap_email = auth_hash.has_email?
gl_user
end
diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb
index 8f3f43c0e91..699d8b9fc07 100644
--- a/lib/gitlab/markdown/pipeline.rb
+++ b/lib/gitlab/markdown/pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Gitlab
module Markdown
class Pipeline
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 2d266ccfe9e..88a265c6af2 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -6,16 +6,20 @@ module Gitlab
METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s
PATH_REGEX = /^#{RAILS_ROOT}\/?/
- def self.pool_size
- current_application_settings[:metrics_pool_size] || 16
- end
-
- def self.timeout
- current_application_settings[:metrics_timeout] || 10
+ def self.settings
+ @settings ||= {
+ enabled: current_application_settings[:metrics_enabled],
+ pool_size: current_application_settings[:metrics_pool_size],
+ timeout: current_application_settings[:metrics_timeout],
+ method_call_threshold: current_application_settings[:metrics_method_call_threshold],
+ host: current_application_settings[:metrics_host],
+ port: current_application_settings[:metrics_port],
+ sample_interval: current_application_settings[:metrics_sample_interval] || 15
+ }
end
def self.enabled?
- current_application_settings[:metrics_enabled] || false
+ settings[:enabled] || false
end
def self.mri?
@@ -26,32 +30,13 @@ module Gitlab
# This is memoized since this method is called for every instrumented
# method. Loading data from an external cache on every method call slows
# things down too much.
- @method_call_threshold ||=
- (current_application_settings[:metrics_method_call_threshold] || 10)
+ @method_call_threshold ||= settings[:method_call_threshold]
end
def self.pool
@pool
end
- def self.hostname
- @hostname
- end
-
- # Returns a relative path and line number based on the last application call
- # frame.
- def self.last_relative_application_frame
- frame = caller_locations.find do |l|
- l.path.start_with?(RAILS_ROOT) && !l.path.start_with?(METRICS_ROOT)
- end
-
- if frame
- return frame.path.sub(PATH_REGEX, ''), frame.lineno
- else
- return nil, nil
- end
- end
-
def self.submit_metrics(metrics)
prepared = prepare_metrics(metrics)
@@ -85,19 +70,15 @@ module Gitlab
value.to_s.gsub('=', '\\=')
end
- @hostname = Socket.gethostname
-
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
- @pool = ConnectionPool.new(size: pool_size, timeout: timeout) do
- host = current_application_settings[:metrics_host]
- user = current_application_settings[:metrics_username]
- pw = current_application_settings[:metrics_password]
- port = current_application_settings[:metrics_port]
+ @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do
+ host = settings[:host]
+ port = settings[:port]
InfluxDB::Client.
- new(udp: { host: host, port: port }, username: user, password: pw)
+ new(udp: { host: host, port: port })
end
end
end
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 06fc2f25948..face1921d2e 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -106,23 +106,41 @@ module Gitlab
if type == :instance
target = mod
label = "#{mod.name}##{name}"
+ method = mod.instance_method(name)
else
target = mod.singleton_class
label = "#{mod.name}.#{name}"
+ method = mod.method(name)
end
+ # Some code out there (e.g. the "state_machine" Gem) checks the arity of
+ # a method to make sure it only passes arguments when the method expects
+ # any. If we were to always overwrite a method to take an `*args`
+ # signature this would break things. As a result we'll make sure the
+ # generated method _only_ accepts regular arguments if the underlying
+ # method also accepts them.
+ if method.arity == 0
+ args_signature = '&block'
+ else
+ args_signature = '*args, &block'
+ end
+
+ send_signature = "__send__(#{alias_name.inspect}, #{args_signature})"
+
target.class_eval <<-EOF, __FILE__, __LINE__ + 1
alias_method #{alias_name.inspect}, #{name.inspect}
- def #{name}(*args, &block)
+ def #{name}(#{args_signature})
trans = Gitlab::Metrics::Instrumentation.transaction
if trans
start = Time.now
- retval = __send__(#{alias_name.inspect}, *args, &block)
+ retval = #{send_signature}
duration = (Time.now - start) * 1000.0
if duration >= Gitlab::Metrics.method_call_threshold
+ trans.increment(:method_duration, duration)
+
trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
{ duration: duration },
method: #{label.inspect})
@@ -130,7 +148,7 @@ module Gitlab
retval
else
- __send__(#{alias_name.inspect}, *args, &block)
+ #{send_signature}
end
end
EOF
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index 753008df99a..7ea9555cc8c 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -17,11 +17,8 @@ module Gitlab
# Returns a Hash in a format that can be directly written to InfluxDB.
def to_hash
{
- series: @series,
- tags: @tags.merge(
- hostname: Metrics.hostname,
- process_type: Sidekiq.server? ? 'sidekiq' : 'rails'
- ),
+ series: @series,
+ tags: @tags,
values: @values,
timestamp: @created_at.to_i * 1_000_000_000
}
diff --git a/lib/gitlab/metrics/obfuscated_sql.rb b/lib/gitlab/metrics/obfuscated_sql.rb
deleted file mode 100644
index fe97d7a0534..00000000000
--- a/lib/gitlab/metrics/obfuscated_sql.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Gitlab
- module Metrics
- # Class for producing SQL queries with sensitive data stripped out.
- class ObfuscatedSQL
- REPLACEMENT = /
- \d+(\.\d+)? # integers, floats
- | '.+?' # single quoted strings
- | \/.+?(?<!\\)\/ # regexps (including escaped slashes)
- /x
-
- MYSQL_REPLACEMENTS = /
- ".+?" # double quoted strings
- /x
-
- # Regex to replace consecutive placeholders with a single one indicating
- # the length. This can be useful when a "IN" statement uses thousands of
- # IDs (storing this would just be a waste of space).
- CONSECUTIVE = /(\?(\s*,\s*)?){2,}/
-
- # sql - The raw SQL query as a String.
- def initialize(sql)
- @sql = sql
- end
-
- # Returns a new, obfuscated SQL query.
- def to_s
- regex = REPLACEMENT
-
- if Gitlab::Database.mysql?
- regex = Regexp.union(regex, MYSQL_REPLACEMENTS)
- end
-
- sql = @sql.gsub(regex, '?').gsub(CONSECUTIVE) do |match|
- "#{match.count(',') + 1} values"
- end
-
- # InfluxDB escapes double quotes upon output, so lets get rid of them
- # whenever we can.
- if Gitlab::Database.postgresql?
- sql = sql.delete('"')
- end
-
- sql.tr("\n", ' ')
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 5c0587c4c51..6f179789d3e 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -32,17 +32,15 @@ module Gitlab
def transaction_from_env(env)
trans = Transaction.new
- trans.add_tag(:request_method, env['REQUEST_METHOD'])
- trans.add_tag(:request_uri, env['REQUEST_URI'])
+ trans.set(:request_uri, env['REQUEST_URI'])
+ trans.set(:request_method, env['REQUEST_METHOD'])
trans
end
def tag_controller(trans, env)
- controller = env[CONTROLLER_KEY]
- label = "#{controller.class.name}##{controller.action_name}"
-
- trans.add_tag(:action, label)
+ controller = env[CONTROLLER_KEY]
+ trans.action = "#{controller.class.name}##{controller.action_name}"
end
end
end
diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb
index 998578e1c0a..fc709222a9b 100644
--- a/lib/gitlab/metrics/sampler.rb
+++ b/lib/gitlab/metrics/sampler.rb
@@ -7,9 +7,14 @@ module Gitlab
# statistics, etc.
class Sampler
# interval - The sampling interval in seconds.
- def initialize(interval = 15)
- @interval = interval
- @metrics = []
+ def initialize(interval = Metrics.settings[:sample_interval])
+ interval_half = interval.to_f / 2
+
+ @interval = interval
+ @interval_steps = (-interval_half..interval_half).step(0.1).to_a
+ @last_step = nil
+
+ @metrics = []
@last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
@last_major_gc = Delta.new(GC.stat[:major_gc_count])
@@ -26,7 +31,7 @@ module Gitlab
Thread.current.abort_on_exception = true
loop do
- sleep(@interval)
+ sleep(sleep_interval)
sample
end
@@ -50,12 +55,11 @@ module Gitlab
end
def sample_memory_usage
- @metrics << Metric.new('memory_usage', value: System.memory_usage)
+ add_metric('memory_usage', value: System.memory_usage)
end
def sample_file_descriptors
- @metrics << Metric.
- new('file_descriptors', value: System.file_descriptor_count)
+ add_metric('file_descriptors', value: System.file_descriptor_count)
end
if Metrics.mri?
@@ -69,7 +73,7 @@ module Gitlab
counts['Symbol'] = Symbol.all_symbols.length
counts.each do |name, count|
- @metrics << Metric.new('object_counts', { count: count }, type: name)
+ add_metric('object_counts', { count: count }, type: name)
end
end
else
@@ -91,7 +95,34 @@ module Gitlab
stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
- @metrics << Metric.new('gc_statistics', stats)
+ add_metric('gc_statistics', stats)
+ end
+
+ def add_metric(series, values, tags = {})
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def sidekiq?
+ Sidekiq.server?
+ end
+
+ # Returns the sleep interval with a random adjustment.
+ #
+ # The random adjustment is put in place to ensure we:
+ #
+ # 1. Don't generate samples at the exact same interval every time (thus
+ # potentially missing anything that happens in between samples).
+ # 2. Don't sample data at the same interval two times in a row.
+ def sleep_interval
+ while step = @interval_steps.sample
+ if step != @last_step
+ @last_step = step
+
+ return @interval + @last_step
+ end
+ end
end
end
end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index ad441decfa2..fd98aa3412e 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -5,19 +5,14 @@ module Gitlab
# This middleware is intended to be used as a server-side middleware.
class SidekiqMiddleware
def call(worker, message, queue)
- trans = Transaction.new
+ trans = Transaction.new("#{worker.class.name}#perform")
begin
trans.run { yield }
ensure
- tag_worker(trans, worker)
trans.finish
end
end
-
- def tag_worker(trans, worker)
- trans.add_tag(:action, "#{worker.class.name}#perform")
- end
end
end
end
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index 7e0dcf99d92..2e9dd4645e3 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -19,6 +19,7 @@ module Gitlab
values = values_for(event)
tags = tags_for(event)
+ current_transaction.increment(:view_duration, event.duration)
current_transaction.add_metric(SERIES, values, tags)
end
@@ -32,16 +33,8 @@ module Gitlab
def tags_for(event)
path = relative_path(event.payload[:identifier])
- tags = { view: path }
- file, line = Metrics.last_relative_application_frame
-
- if file and line
- tags[:file] = file
- tags[:line] = line
- end
-
- tags
+ { view: path }
end
def current_transaction
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index d947c128ce2..8008b3bc895 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -1,44 +1,18 @@
module Gitlab
module Metrics
module Subscribers
- # Class for tracking raw SQL queries.
- #
- # Queries are obfuscated before being logged to ensure no private data is
- # exposed via InfluxDB/Grafana.
+ # Class for tracking the total query duration of a transaction.
class ActiveRecord < ActiveSupport::Subscriber
attach_to :active_record
- SERIES = 'sql_queries'
-
def sql(event)
return unless current_transaction
- values = values_for(event)
- tags = tags_for(event)
-
- current_transaction.add_metric(SERIES, values, tags)
+ current_transaction.increment(:sql_duration, event.duration)
end
private
- def values_for(event)
- { duration: event.duration }
- end
-
- def tags_for(event)
- sql = ObfuscatedSQL.new(event.payload[:sql]).to_s
- tags = { sql: sql }
-
- file, line = Metrics.last_relative_application_frame
-
- if file and line
- tags[:file] = file
- tags[:line] = line
- end
-
- tags
- end
-
def current_transaction
Transaction.current
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index a61dbd989e7..2578ddc49f4 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -4,45 +4,64 @@ module Gitlab
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
- SERIES = 'transactions'
+ attr_reader :tags, :values
- attr_reader :uuid, :tags
+ attr_accessor :action
def self.current
Thread.current[THREAD_KEY]
end
- # name - The name of this transaction as a String.
- def initialize
+ # action - A String describing the action performed, usually the class
+ # plus method name.
+ def initialize(action = nil)
@metrics = []
- @uuid = SecureRandom.uuid
@started_at = nil
@finished_at = nil
- @tags = {}
+ @values = Hash.new(0)
+ @tags = {}
+ @action = action
+
+ @memory_before = 0
+ @memory_after = 0
end
def duration
@finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0
end
+ def allocated_memory
+ @memory_after - @memory_before
+ end
+
def run
Thread.current[THREAD_KEY] = self
- @started_at = Time.now
+ @memory_before = System.memory_usage
+ @started_at = Time.now
yield
ensure
- @finished_at = Time.now
+ @memory_after = System.memory_usage
+ @finished_at = Time.now
Thread.current[THREAD_KEY] = nil
end
def add_metric(series, values, tags = {})
- tags = tags.merge(transaction_id: @uuid)
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def increment(name, value)
+ @values[name] += value
+ end
- @metrics << Metric.new(series, values, tags)
+ def set(name, value)
+ @values[name] = value
end
def add_tag(key, value)
@@ -55,11 +74,29 @@ module Gitlab
end
def track_self
- add_metric(SERIES, { duration: duration }, @tags)
+ values = { duration: duration, allocated_memory: allocated_memory }
+
+ @values.each do |name, value|
+ values[name] = value
+ end
+
+ add_metric('transactions', values, @tags)
end
def submit
- Metrics.submit_metrics(@metrics.map(&:to_hash))
+ metrics = @metrics.map do |metric|
+ hash = metric.to_hash
+
+ hash[:tags][:action] ||= @action if @action
+
+ hash
+ end
+
+ Metrics.submit_metrics(metrics)
+ end
+
+ def sidekiq?
+ Sidekiq.server?
end
end
end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
new file mode 100644
index 00000000000..50b0dd32380
--- /dev/null
+++ b/lib/gitlab/middleware/go.rb
@@ -0,0 +1,50 @@
+# A dumb middleware that returns a Go HTML document if the go-get=1 query string
+# is used irrespective if the namespace/project exists
+module Gitlab
+ module Middleware
+ class Go
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = Rack::Request.new(env)
+
+ if go_request?(request)
+ render_go_doc(request)
+ else
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def render_go_doc(request)
+ body = go_body(request)
+ response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' })
+ response.finish
+ end
+
+ def go_request?(request)
+ request["go-get"].to_i == 1 && request.env["PATH_INFO"].present?
+ end
+
+ def go_body(request)
+ base_url = Gitlab.config.gitlab.url
+ # Go subpackages may be in the form of namespace/project/path1/path2/../pathN
+ # We can just ignore the paths and leave the namespace/project
+ path_info = request.env["PATH_INFO"]
+ path_info.sub!(/^\//, '')
+ project_path = path_info.split('/').first(2).join('/')
+ request_url = URI.join(base_url, project_path)
+ domain_path = strip_url(request_url.to_s)
+
+ "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n";
+ end
+
+ def strip_url(url)
+ url.gsub(/\Ahttps?:\/\//, '')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb
index ea6b0ee796d..71cf6a0d886 100644
--- a/lib/gitlab/note_data_builder.rb
+++ b/lib/gitlab/note_data_builder.rb
@@ -53,13 +53,10 @@ module Gitlab
object_kind: "note",
user: user.hook_attrs,
project_id: project.id,
- repository: {
- name: project.name,
- url: project.url_to_repo,
- description: project.description,
- homepage: project.web_url,
- },
- object_attributes: note.hook_attrs
+ project: project.hook_attrs,
+ object_attributes: note.hook_attrs,
+ # DEPRECATED
+ repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
base_data[:object_attributes][:url] =
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index ba31599432b..36e5c2670bb 100644
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -32,6 +32,10 @@ module Gitlab
@password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase)
end
+ def has_email?
+ get_info(:email).present?
+ end
+
private
def info
@@ -46,8 +50,8 @@ module Gitlab
def username_and_email
@username_and_email ||= begin
- username = get_info(:username) || get_info(:nickname)
- email = get_info(:email)
+ username = get_info(:username).presence || get_info(:nickname).presence
+ email = get_info(:email).presence
username ||= generate_username(email) if email
email ||= generate_temporarily_email(username) if username
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index f1a362f5303..832fb08a526 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -26,7 +26,7 @@ module Gitlab
gl_user.try(:valid?)
end
- def save
+ def save(provider = 'OAuth')
unauthorized_to_create unless gl_user
if needs_blocking?
@@ -36,10 +36,10 @@ module Gitlab
gl_user.save!
end
- log.info "(OAuth) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
+ log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
gl_user
rescue ActiveRecord::RecordInvalid => e
- log.info "(OAuth) Error saving user: #{gl_user.errors.full_messages}"
+ log.info "(#{provider}) Error saving user: #{gl_user.errors.full_messages}"
return self, e.record.errors
end
@@ -105,13 +105,18 @@ module Gitlab
end
def signup_enabled?
- Gitlab.config.omniauth.allow_single_sign_on
+ providers = Gitlab.config.omniauth.allow_single_sign_on
+ if providers.is_a?(Array)
+ providers.include?(auth_hash.provider)
+ else
+ providers
+ end
end
def block_after_signup?
if creating_linked_ldap_user?
ldap_config.block_auto_created_users
- else
+ else
Gitlab.config.omniauth.block_auto_created_users
end
end
@@ -135,15 +140,18 @@ module Gitlab
def user_attributes
# Give preference to LDAP for sensitive information when creating a linked account
if creating_linked_ldap_user?
- username = ldap_person.username
- email = ldap_person.email.first
- else
- username = auth_hash.username
- email = auth_hash.email
+ username = ldap_person.username.presence
+ email = ldap_person.email.first.presence
end
-
+
+ username ||= auth_hash.username
+ email ||= auth_hash.email
+
+ name = auth_hash.name
+ name = ::Namespace.clean_path(username) if name.strip.empty?
+
{
- name: auth_hash.name,
+ name: name,
username: ::Namespace.clean_path(username),
email: email,
password: auth_hash.password,
diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb
new file mode 100644
index 00000000000..746ec283330
--- /dev/null
+++ b/lib/gitlab/other_markup.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ # Parser/renderer for markups without other special support code.
+ module OtherMarkup
+
+ # Public: Converts the provided markup into HTML.
+ #
+ # input - the source text in a markup format
+ # context - a Hash with the template context:
+ # :commit
+ # :project
+ # :project_wiki
+ # :requested_path
+ # :ref
+ #
+ def self.render(file_name, input, context)
+ html = GitHub::Markup.render(file_name, input).
+ force_encoding(input.encoding)
+
+ html = Banzai.post_process(html, context)
+
+ html.html_safe
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 70de6a74e76..71c5b6801fb 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -2,8 +2,9 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
- def initialize(project_id, query, repository_ref = nil)
- @project = Project.find(project_id)
+ def initialize(current_user, project, query, repository_ref = nil)
+ @current_user = current_user
+ @project = project
@repository_ref = if repository_ref.present?
repository_ref
else
@@ -73,7 +74,7 @@ module Gitlab
end
def notes
- Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC')
+ project.notes.user.search(query).order('updated_at DESC')
end
def commits
@@ -84,8 +85,8 @@ module Gitlab
end
end
- def limit_project_ids
- [project.id]
+ def project_ids_relation
+ project
end
end
end
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb
index 4f9cdef3869..97d1edab9c1 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/push_data_builder.rb
@@ -22,6 +22,8 @@ module Gitlab
# }
#
def build(project, user, oldrev, newrev, ref, commits = [], message = nil)
+ commits = Array(commits)
+
# Total commits count
commits_count = commits.size
@@ -47,25 +49,21 @@ module Gitlab
user_id: user.id,
user_name: user.name,
user_email: user.email,
+ user_avatar: user.avatar_url,
project_id: project.id,
- repository: {
- name: project.name,
- url: project.url_to_repo,
- description: project.description,
- homepage: project.web_url,
- git_http_url: project.http_url_to_repo,
- git_ssh_url: project.ssh_url_to_repo,
- visibility_level: project.visibility_level
- },
+ project: project.hook_attrs,
commits: commit_attrs,
- total_commits_count: commits_count
+ total_commits_count: commits_count,
+ # DEPRECATED
+ repository: project.hook_attrs.slice(:name, :url, :description, :homepage,
+ :git_http_url, :git_ssh_url, :visibility_level)
}
data
end
# This method provide a sample data generated with
- # existing project and commits to test web hooks
+ # existing project and commits to test webhooks
def build_sample(project, user)
commits = project.repository.commits(project.default_branch, nil, 3)
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
diff --git a/lib/gitlab/redis_config.rb b/lib/gitlab/redis_config.rb
new file mode 100644
index 00000000000..4949c6db539
--- /dev/null
+++ b/lib/gitlab/redis_config.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ class RedisConfig
+ attr_reader :url
+
+ def self.url
+ new.url
+ end
+
+ def self.redis_store_options
+ url = new.url
+ redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(url)
+ # Redis::Store does not handle Unix sockets well, so let's do it for them
+ redis_uri = URI.parse(url)
+ if redis_uri.scheme == 'unix'
+ redis_config_hash[:path] = redis_uri.path
+ end
+ redis_config_hash
+ end
+
+ def initialize(rails_env=nil)
+ rails_env ||= Rails.env
+ config_file = File.expand_path('../../../config/resque.yml', __FILE__)
+
+ @url = "redis://localhost:6379"
+ if File.exists?(config_file)
+ @url =YAML.load_file(config_file)[rails_env]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index be795649e59..4d830aa45e1 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
@@ -19,7 +17,7 @@ module Gitlab
super(text, context.merge(project: project))
end
- %i(user label merge_request snippet commit commit_range).each do |type|
+ %i(user label milestone merge_request snippet commit commit_range).each do |type|
define_method("#{type}s") do
@references[type] ||= references(type, reference_context)
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 53ab2686b43..ace906a6f59 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -34,29 +34,29 @@ module Gitlab
def project_path_regex
- @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git)\z/.freeze
+ @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git|\.atom)\z/.freeze
end
def project_path_regex_message
"can contain only letters, digits, '_', '-' and '.'. " \
- "Cannot start with '-' or end in '.git'" \
+ "Cannot start with '-', end in '.git' or end in '.atom'" \
end
def file_name_regex
- @file_name_regex ||= /\A[a-zA-Z0-9_\-\.]*\z/.freeze
+ @file_name_regex ||= /\A[a-zA-Z0-9_\-\.\@]*\z/.freeze
end
def file_name_regex_message
- "can contain only letters, digits, '_', '-' and '.'. "
+ "can contain only letters, digits, '_', '-', '@' and '.'. "
end
def file_path_regex
- @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/]*\z/.freeze
+ @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/\@]*\z/.freeze
end
def file_path_regex_message
- "can contain only letters, digits, '_', '-' and '.'. Separate directories with a '/'. "
+ "can contain only letters, digits, '_', '-', '@' and '.'. Separate directories with a '/'. "
end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
new file mode 100644
index 00000000000..b1e30110ef5
--- /dev/null
+++ b/lib/gitlab/saml/user.rb
@@ -0,0 +1,47 @@
+# SAML extension for User model
+#
+# * Find GitLab user based on SAML uid and provider
+# * Create new user from SAML data
+#
+module Gitlab
+ module Saml
+ class User < Gitlab::OAuth::User
+
+ def save
+ super('SAML')
+ end
+
+ def gl_user
+ @user ||= find_by_uid_and_provider
+
+ if auto_link_ldap_user?
+ @user ||= find_or_create_ldap_user
+ end
+
+ if auto_link_saml_enabled?
+ @user ||= find_by_email
+ end
+
+ if signup_enabled?
+ @user ||= build_new_user
+ end
+
+ @user
+ end
+
+ def find_by_email
+ if auth_hash.has_email?
+ user = ::User.find_by(email: auth_hash.email.downcase)
+ user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider) if user
+ user
+ end
+ end
+
+ protected
+
+ def auto_link_saml_enabled?
+ Gitlab.config.omniauth.auto_link_saml_user
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 2ab2d4af797..f8ab2b1f09e 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -1,13 +1,14 @@
module Gitlab
class SearchResults
- attr_reader :query
+ attr_reader :current_user, :query
- # Limit search results by passed project ids
+ # Limit search results by passed projects
# It allows us to search only for projects user has access to
- attr_reader :limit_project_ids
+ attr_reader :limit_projects
- def initialize(limit_project_ids, query)
- @limit_project_ids = limit_project_ids || Project.all
+ 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
@@ -27,7 +28,8 @@ module Gitlab
end
def total_count
- @total_count ||= projects_count + issues_count + merge_requests_count + milestones_count
+ @total_count ||= projects_count + issues_count + merge_requests_count +
+ milestones_count
end
def projects_count
@@ -53,27 +55,29 @@ module Gitlab
private
def projects
- Project.where(id: limit_project_ids).search(query)
+ limit_projects.search(query)
end
def issues
- issues = Issue.where(project_id: limit_project_ids)
+ issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation)
+
if query =~ /#(\d+)\z/
issues = issues.where(iid: $1)
else
issues = issues.full_search(query)
end
+
issues.order('updated_at DESC')
end
def milestones
- milestones = Milestone.where(project_id: limit_project_ids)
+ milestones = Milestone.where(project_id: project_ids_relation)
milestones = milestones.search(query)
milestones.order('updated_at DESC')
end
def merge_requests
- merge_requests = MergeRequest.in_projects(limit_project_ids)
+ merge_requests = MergeRequest.in_projects(project_ids_relation)
if query =~ /[#!](\d+)\z/
merge_requests = merge_requests.where(iid: $1)
else
@@ -89,5 +93,9 @@ module Gitlab
def per_page
20
end
+
+ def project_ids_relation
+ limit_projects.select(:id).reorder(nil)
+ end
end
end
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index 938219efdb2..e0e74ff8359 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -1,18 +1,20 @@
module Gitlab
class SnippetSearchResults < SearchResults
- attr_reader :limit_snippet_ids
+ include SnippetsHelper
- def initialize(limit_snippet_ids, query)
- @limit_snippet_ids = limit_snippet_ids
+ attr_reader :limit_snippets
+
+ def initialize(limit_snippets, query)
+ @limit_snippets = limit_snippets
@query = query
end
def objects(scope, page = nil)
case scope
when 'snippet_titles'
- Kaminari.paginate_array(snippet_titles).page(page).per(per_page)
+ snippet_titles.page(page).per(per_page)
when 'snippet_blobs'
- Kaminari.paginate_array(snippet_blobs).page(page).per(per_page)
+ snippet_blobs.page(page).per(per_page)
else
super
end
@@ -33,99 +35,15 @@ module Gitlab
private
def snippet_titles
- Snippet.where(id: limit_snippet_ids).search(query).order('updated_at DESC')
+ limit_snippets.search(query).order('updated_at DESC')
end
def snippet_blobs
- search = Snippet.where(id: limit_snippet_ids).search_code(query)
- search = search.order('updated_at DESC').to_a
- snippets = []
- search.each { |e| snippets << chunk_snippet(e) }
- snippets
+ limit_snippets.search_code(query).order('updated_at DESC')
end
def default_scope
'snippet_blobs'
end
-
- # Get an array of line numbers surrounding a matching
- # line, bounded by min/max.
- #
- # @returns Array of line numbers
- def bounded_line_numbers(line, min, max)
- lower = line - surrounding_lines > min ? line - surrounding_lines : min
- upper = line + surrounding_lines < max ? line + surrounding_lines : max
- (lower..upper).to_a
- end
-
- # Returns a sorted set of lines to be included in a snippet preview.
- # This ensures matching adjacent lines do not display duplicated
- # surrounding code.
- #
- # @returns Array, unique and sorted.
- def matching_lines(lined_content)
- used_lines = []
- lined_content.each_with_index do |line, line_number|
- used_lines.concat bounded_line_numbers(
- line_number,
- 0,
- lined_content.size
- ) if line.include?(query)
- end
-
- used_lines.uniq.sort
- end
-
- # 'Chunkify' entire snippet. Splits the snippet data into matching lines +
- # surrounding_lines() worth of unmatching lines.
- #
- # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}}
- def chunk_snippet(snippet)
- lined_content = snippet.content.split("\n")
- used_lines = matching_lines(lined_content)
-
- snippet_chunk = []
- snippet_chunks = []
- snippet_start_line = 0
- last_line = -1
-
- # Go through each used line, and add consecutive lines as a single chunk
- # to the snippet chunk array.
- used_lines.each do |line_number|
- if last_line < 0
- # Start a new chunk.
- snippet_start_line = line_number
- snippet_chunk << lined_content[line_number]
- elsif last_line == line_number - 1
- # Consecutive line, continue chunk.
- snippet_chunk << lined_content[line_number]
- else
- # Non-consecutive line, add chunk to chunk array.
- snippet_chunks << {
- data: snippet_chunk.join("\n"),
- start_line: snippet_start_line + 1
- }
-
- # Start a new chunk.
- snippet_chunk = [lined_content[line_number]]
- snippet_start_line = line_number
- end
- last_line = line_number
- end
- # Add final chunk to chunk array
- snippet_chunks << {
- data: snippet_chunk.join("\n"),
- start_line: snippet_start_line + 1
- }
-
- # Return snippet with chunk array
- { snippet_object: snippet, snippet_chunks: snippet_chunks }
- end
-
- # Defines how many unmatching lines should be
- # included around the matching lines in a snippet
- def surrounding_lines
- 3
- end
end
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 4885baf9526..d1b42c1f9b9 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -3,7 +3,7 @@ module Gitlab
def self.allowed?(user)
return false if user.blocked?
- if user.requires_ldap_check?
+ if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
new file mode 100644
index 00000000000..c3ddd4c2680
--- /dev/null
+++ b/lib/gitlab/workhorse.rb
@@ -0,0 +1,40 @@
+require 'base64'
+require 'json'
+
+module Gitlab
+ class Workhorse
+ SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
+
+ class << self
+ def send_git_blob(repository, blob)
+ params = {
+ 'RepoPath' => repository.path_to_repo,
+ 'BlobId' => blob.id,
+ }
+
+ [
+ SEND_DATA_HEADER,
+ "git-blob:#{encode(params)}",
+ ]
+ end
+
+ def send_git_archive(project, ref, format)
+ format ||= 'tar.gz'
+ format.downcase!
+ params = project.repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
+ raise "Repository or ref not found" if params.empty?
+
+ [
+ SEND_DATA_HEADER,
+ "git-archive:#{encode(params)}",
+ ]
+ end
+
+ protected
+
+ def encode(hash)
+ Base64.urlsafe_encode64(JSON.dump(hash))
+ end
+ end
+ end
+end
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index c5f07c8b508..d95e7023d2e 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -38,6 +38,7 @@ web_server_pid_path="$pid_path/unicorn.pid"
sidekiq_pid_path="$pid_path/sidekiq.pid"
mail_room_enabled=false
mail_room_pid_path="$pid_path/mail_room.pid"
+gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
@@ -48,7 +49,7 @@ test -f /etc/default/gitlab && . /etc/default/gitlab
# Switch to the app_user if it is not he/she who is running the script.
if [ `whoami` != "$app_user" ]; then
- eval su - "$app_user" -s $shell_path -c $(echo \")$0 "$@"$(echo \"); exit;
+ eval su - "$app_user" -c $(echo \")$shell_path -l -c \'$0 "$@"\'$(echo \"); exit;
fi
# Switch to the gitlab path, exit on failure.
@@ -218,7 +219,7 @@ start_gitlab() {
echo "The Unicorn web server already running with pid $wpid, not restarting."
else
# Remove old socket if it exists
- rm -f "$socket_path"/gitlab.socket 2>/dev/null
+ rm -f "$rails_socket" 2>/dev/null
# Start the web server
RAILS_ENV=$RAILS_ENV bin/web start
fi
@@ -233,10 +234,12 @@ start_gitlab() {
if [ "$gitlab_workhorse_status" = "0" ]; then
echo "The gitlab-workhorse is already running with pid $spid, not restarting"
else
- # No need to remove a socket, gitlab-workhorse does this itself
+ # No need to remove a socket, gitlab-workhorse does this itself.
+ # Because gitlab-workhorse has multiple executables we need to fix
+ # the PATH.
$app_root/bin/daemon_with_pidfile $gitlab_workhorse_pid_path \
- $app_root/../gitlab-workhorse/gitlab-workhorse \
- $gitlab_workhorse_options \
+ /usr/bin/env PATH=$gitlab_workhorse_dir:$PATH \
+ gitlab-workhorse $gitlab_workhorse_options \
>> $gitlab_workhorse_log 2>&1 &
fi
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index 1937ca582b0..cc8617b72ca 100755
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -30,12 +30,20 @@ web_server_pid_path="$pid_path/unicorn.pid"
# The default is "$pid_path/sidekiq.pid"
sidekiq_pid_path="$pid_path/sidekiq.pid"
+# The directory where the gitlab-workhorse binaries are. Usually
+# /home/git/gitlab-workhorse .
+gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse && pwd)
gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
+
# The -listenXxx settings determine where gitlab-workhorse
-# listens for connections from NGINX. To listen on localhost:8181, write
-# '-listenNetwork tcp -listenAddr localhost:8181'.
-# The -authBackend setting tells gitlab-workhorse where it can reach
-# Unicorn.
+# listens for connections from the web server. By default it listens to a
+# socket. To listen on TCP connections (needed by Apache) change to:
+# '-listenNetwork tcp -listenAddr 127.0.0.1:8181'
+#
+# The -authBackend setting tells gitlab-workhorse where it can reach Unicorn.
+# For relative URL support change to:
+# '-authBackend http://127.0.0.1/8080/gitlab'
+# Read more in http://doc.gitlab.com/ce/install/relative_url.html
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index fc5475c4eef..1324e4cd267 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -30,7 +30,6 @@ server {
listen [::]:80 default_server;
server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com
server_tokens off; ## Don't show the nginx version number, a security best practice
- root /home/git/gitlab/public;
## See app/controllers/application_controller.rb for headers set
@@ -57,4 +56,14 @@ server {
proxy_pass http://gitlab-workhorse;
}
+
+ error_page 404 /404.html;
+ error_page 422 /422.html;
+ error_page 500 /500.html;
+ error_page 502 /502.html;
+ location ~ ^/(404|422|500|502)\.html$ {
+ root /home/git/gitlab/public;
+ internal;
+ }
+
}
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 1e5f85413ec..af6ea9ed706 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -45,7 +45,6 @@ server {
listen [::]:443 ipv6only=on ssl default_server;
server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com
server_tokens off; ## Don't show the nginx version number, a security best practice
- root /home/git/gitlab/public;
## Strong SSL Security
## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
@@ -101,4 +100,13 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://gitlab-workhorse;
}
+
+ error_page 404 /404.html;
+ error_page 422 /422.html;
+ error_page 500 /500.html;
+ error_page 502 /502.html;
+ location ~ ^/(404|422|500|502)\.html$ {
+ root /home/git/gitlab/public;
+ internal;
+ }
}
diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake
index 5d4e0740373..d5a402907d8 100644
--- a/lib/tasks/brakeman.rake
+++ b/lib/tasks/brakeman.rake
@@ -2,7 +2,7 @@ desc 'Security check via brakeman'
task :brakeman do
# We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge
# requests are welcome!
- if system(*%W(brakeman --skip-files lib/backup/repository.rb -w3 -z))
+ if system(*%W(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
puts 'Security check succeed'
else
puts 'Security check failed'
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 1728dda72cf..51e746ef923 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,11 +1,21 @@
namespace :cache do
+ CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
+ REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
+
desc "GitLab | Clear redis cache"
task :clear => :environment do
- # Hack into Rails.cache until https://github.com/redis-store/redis-store/pull/225
- # is accepted (I hope) and we can update the redis-store gem.
- redis_store = Rails.cache.instance_variable_get(:@data)
- redis_store.keys.each_slice(1000) do |key_slice|
- redis_store.del(*key_slice)
+ redis = Redis.new(url: Gitlab::RedisConfig.url)
+ cursor = REDIS_SCAN_START_STOP
+ loop do
+ cursor, keys = redis.scan(
+ cursor,
+ match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*",
+ count: CLEAR_BATCH_SIZE
+ )
+
+ redis.del(*keys) if keys.any?
+
+ break if cursor == REDIS_SCAN_START_STOP
end
end
end
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
new file mode 100644
index 00000000000..cfaf4a129b1
--- /dev/null
+++ b/lib/tasks/gemojione.rake
@@ -0,0 +1,122 @@
+# 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
+ task sprite: :environment do
+ check_requirements!
+
+ SIZE = 20
+ RETINA = SIZE * 2
+
+ Dir.mktmpdir do |tmpdir|
+ # Copy the Gemojione assets to the temporary folder for resizing
+ FileUtils.cp_r(Gemojione.index.images_path, tmpdir)
+
+ Dir.chdir(tmpdir) do
+ Dir["**/*.png"].each do |png|
+ resize!(File.join(tmpdir, png), SIZE)
+ end
+ end
+
+ style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss))
+
+ # Combine the resized assets into a packed sprite and re-generate the SCSS
+ SpriteFactory.cssurl = "image-url('$IMAGE')"
+ SpriteFactory.run!(File.join(tmpdir, 'images'), {
+ output_style: style_path,
+ output_image: "app/assets/images/emoji.png",
+ selector: '.emoji-',
+ style: :scss,
+ nocomments: true,
+ pngcrush: true,
+ layout: :packed
+ })
+
+ # SpriteFactory's SCSS is a bit too verbose for our purposes here, so
+ # let's simplify it
+ system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
+ system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
+ system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path}))
+
+ # Append a generic rule that applies to all Emojis
+ File.open(style_path, 'a') do |f|
+ f.puts
+ f.puts <<-CSS.strip_heredoc
+ .emoji-icon {
+ background-image: image-url('emoji.png');
+ background-repeat: no-repeat;
+ height: #{SIZE}px;
+ width: #{SIZE}px;
+
+ @media only screen and (-webkit-min-device-pixel-ratio: 2),
+ only screen and (min--moz-device-pixel-ratio: 2),
+ only screen and (-o-min-device-pixel-ratio: 2/1),
+ only screen and (min-device-pixel-ratio: 2),
+ only screen and (min-resolution: 192dpi),
+ only screen and (min-resolution: 2dppx) {
+ background-image: image-url('emoji@2x.png');
+ background-size: 840px 820px;
+ }
+ }
+ CSS
+ end
+ end
+
+ # Now do it again but for Retina
+ Dir.mktmpdir do |tmpdir|
+ # Copy the Gemojione assets to the temporary folder for resizing
+ FileUtils.cp_r(Gemojione.index.images_path, tmpdir)
+
+ Dir.chdir(tmpdir) do
+ Dir["**/*.png"].each do |png|
+ resize!(File.join(tmpdir, png), RETINA)
+ end
+ end
+
+ # Combine the resized assets into a packed sprite and re-generate the SCSS
+ SpriteFactory.run!(File.join(tmpdir, 'images'), {
+ output_image: "app/assets/images/emoji@2x.png",
+ style: false,
+ nocomments: true,
+ pngcrush: true,
+ layout: :packed
+ })
+ end
+ end
+
+ def check_requirements!
+ return if defined?(SpriteFactory) && defined?(Magick)
+
+ puts <<-MSG.strip_heredoc
+ This task is disabled by default and should only be run when the Gemojione
+ gem is updated with new Emojis.
+
+ To enable this task, *temporarily* add the following lines to Gemfile and
+ re-bundle:
+
+ gem 'sprite-factory'
+ gem 'rmagick'
+ MSG
+
+ exit 1
+ end
+
+ def resize!(image_path, size)
+ # Resize the image in-place, save it, and free the object
+ image = Magick::Image.read(image_path).first
+ image.resize!(size, size)
+ image.write(image_path) { self.quality = 100 }
+ image.destroy!
+ end
+end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 0469c5a61c3..27ed57efe55 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -16,7 +16,6 @@ namespace :gitlab do
check_git_config
check_database_config_exists
- check_database_is_not_sqlite
check_migrations_are_up
check_orphaned_group_members
check_gitlab_config_exists
@@ -90,24 +89,6 @@ namespace :gitlab do
end
end
- def check_database_is_not_sqlite
- print "Database is SQLite ... "
-
- database_config_file = Rails.root.join("config", "database.yml")
-
- unless File.read(database_config_file) =~ /adapter:\s+sqlite/
- puts "no".green
- else
- puts "yes".red
- puts "Please fix this by removing the SQLite entry from the database.yml".blue
- for_more_information(
- "https://github.com/gitlabhq/gitlabhq/wiki/Migrate-from-SQLite-to-MySQL",
- see_database_guide
- )
- fix_and_rerun
- end
- end
-
def check_gitlab_config_exists
print "GitLab config exists? ... "
@@ -285,7 +266,7 @@ namespace :gitlab do
unless File.directory?(Rails.root.join('public/uploads'))
puts "no".red
try_fixing_it(
- "sudo -u #{gitlab_user} mkdir -m 750 #{Rails.root}/public/uploads"
+ "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads"
)
for_more_information(
see_installation_guide_section "GitLab"
@@ -297,21 +278,22 @@ namespace :gitlab do
upload_path = File.realpath(Rails.root.join('public/uploads'))
upload_path_tmp = File.join(upload_path, 'tmp')
- if File.stat(upload_path).mode == 040750
+ if File.stat(upload_path).mode == 040700
unless Dir.exists?(upload_path_tmp)
puts 'skipped (no tmp uploads folder yet)'.magenta
return
end
- # if tmp upload dir has incorrect permissions, assume others do as well
- if File.stat(upload_path_tmp).mode == 040755 && File.owned?(upload_path_tmp) # verify drwxr-xr-x permissions
+ # If tmp upload dir has incorrect permissions, assume others do as well
+ # Verify drwx------ permissions
+ if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp)
puts "yes".green
else
puts "no".red
try_fixing_it(
"sudo chown -R #{gitlab_user} #{upload_path}",
"sudo find #{upload_path} -type f -exec chmod 0644 {} \\;",
- "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0755 {} \\;"
+ "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;"
)
for_more_information(
see_installation_guide_section "GitLab"
@@ -321,7 +303,7 @@ namespace :gitlab do
else
puts "no".red
try_fixing_it(
- "sudo chmod 0750 #{upload_path}",
+ "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;"
)
for_more_information(
see_installation_guide_section "GitLab"
@@ -431,7 +413,7 @@ namespace :gitlab do
try_fixing_it(
"sudo chmod -R ug+rwX,o-rwx #{repo_base_path}",
"sudo chmod -R ug-s #{repo_base_path}",
- "find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s"
+ "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s"
)
for_more_information(
see_installation_guide_section "GitLab Shell"
@@ -746,13 +728,15 @@ namespace :gitlab do
def check_imap_authentication
print "IMAP server credentials are correct? ... "
- config = Gitlab.config.incoming_email
+ config_path = Rails.root.join('config', 'mail_room.yml')
+ config_file = YAML.load(ERB.new(File.read(config_path)).result)
+ config = config_file[:mailboxes].first
if config
begin
- imap = Net::IMAP.new(config.host, port: config.port, ssl: config.ssl)
- imap.starttls if config.start_tls
- imap.login(config.user, config.password)
+ imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
+ imap.starttls if config[:start_tls]
+ imap.login(config[:email], config[:password])
connected = true
rescue
connected = false
@@ -929,7 +913,7 @@ namespace :gitlab do
end
def check_git_version
- required_version = Gitlab::VersionInfo.new(1, 7, 10)
+ required_version = Gitlab::VersionInfo.new(2, 7, 3)
current_version = Gitlab::VersionInfo.parse(run(%W(#{Gitlab.config.git.bin_path} --version)))
puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
index ebe516ec879..d33b5b31e18 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -2,6 +2,11 @@ module Gitlab
class TaskAbortedByUserError < StandardError; end
end
+String.disable_colorization = true unless STDOUT.isatty
+
+# Prevent StateMachine warnings from outputting during a cron task
+StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
+
namespace :gitlab do
# Ask if the user wants to continue
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index 76e443e55ee..cc0f668474e 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -1,13 +1,13 @@
namespace :gitlab do
namespace :web_hook do
- desc "GitLab | Adds a web hook to the projects"
+ desc "GitLab | Adds a webhook to the projects"
task :add => :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
projects = find_projects(namespace_path)
- puts "Adding web hook '#{web_hook_url}' to:"
+ puts "Adding webhook '#{web_hook_url}' to:"
projects.find_each(batch_size: 1000) do |project|
print "- #{project.name} ... "
web_hook = project.hooks.new(url: web_hook_url)
@@ -20,7 +20,7 @@ namespace :gitlab do
end
end
- desc "GitLab | Remove a web hook from the projects"
+ desc "GitLab | Remove a webhook from the projects"
task :rm => :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -28,12 +28,12 @@ namespace :gitlab do
projects = find_projects(namespace_path)
projects_ids = projects.pluck(:id)
- puts "Removing web hooks with the url '#{web_hook_url}' ... "
+ puts "Removing webhooks with the url '#{web_hook_url}' ... "
count = WebHook.where(url: web_hook_url, project_id: projects_ids, type: 'ProjectHook').delete_all
- puts "#{count} web hooks were removed."
+ puts "#{count} webhooks were removed."
end
- desc "GitLab | List web hooks"
+ desc "GitLab | List webhooks"
task :list => :environment do
namespace_path = ENV['NAMESPACE']
@@ -43,7 +43,7 @@ namespace :gitlab do
puts "#{hook.project.name.truncate(20).ljust(20)} -> #{hook.url}"
end
- puts "\n#{web_hooks.size} web hooks found."
+ puts "\n#{web_hooks.size} webhooks found."
end
end
diff --git a/lib/tasks/scss-lint.rake b/lib/tasks/scss-lint.rake
new file mode 100644
index 00000000000..250fd8699e4
--- /dev/null
+++ b/lib/tasks/scss-lint.rake
@@ -0,0 +1,10 @@
+unless Rails.env.production?
+ require 'scss_lint/rake_task'
+
+ SCSSLint::RakeTask.new do |t|
+ t.config = '.scss-lint.yml'
+ # See https://github.com/brigade/scss-lint/issues/726
+ # Hack, otherwise linter won't respect scss_files option in config file.
+ t.files = []
+ end
+end
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 0985ef3a669..2cf7a25a0fd 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -46,20 +46,11 @@ namespace :spec do
run_commands(cmds)
end
- desc 'GitLab | Rspec | Run benchmark specs'
- task :benchmark do
- cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @benchmark)
- ]
- run_commands(cmds)
- end
-
desc 'GitLab | Rspec | Run other specs'
task :other do
cmds = [
%W(rake gitlab:setup),
- %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services --tag ~@benchmark)
+ %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
]
run_commands(cmds)
end
@@ -69,7 +60,7 @@ desc "GitLab | Run specs"
task :spec do
cmds = [
%W(rake gitlab:setup),
- %W(rspec spec --tag ~@benchmark),
+ %W(rspec spec),
]
run_commands(cmds)
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index 3acfc6e2075..01d23b89bb7 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -4,53 +4,59 @@ namespace :spinach do
namespace :project do
desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features"
task :half do
- cmds = [
- %W(rake gitlab:setup),
- %W(spinach --tags @project_commits,@project_issues,@project_merge_requests),
- ]
- run_commands(cmds)
+ run_spinach_tests('@project_commits,@project_issues,@project_merge_requests')
end
desc "GitLab | Spinach | Run remaining project spinach features"
task :rest do
- cmds = [
- %W(rake gitlab:setup),
- %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests),
- ]
- run_commands(cmds)
+ run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests')
end
end
desc "GitLab | Spinach | Run project spinach features"
task :project do
- cmds = [
- %W(rake gitlab:setup),
- %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets),
- ]
- run_commands(cmds)
+ run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets')
end
desc "GitLab | Spinach | Run other spinach features"
task :other do
- cmds = [
- %W(rake gitlab:setup),
- %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets),
- ]
- run_commands(cmds)
+ run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets')
+ end
+
+ desc "GitLab | Spinach | Run other spinach features"
+ task :builds do
+ run_spinach_tests('@builds')
end
end
desc "GitLab | Run spinach"
task :spinach do
- cmds = [
- %W(rake gitlab:setup),
- %W(spinach),
- ]
- run_commands(cmds)
+ run_spinach_tests(nil)
+end
+
+def run_command(cmd)
+ system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
end
-def run_commands(cmds)
- cmds.each do |cmd|
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+def run_spinach_command(args)
+ run_command(%w(spinach -r rerun) + args)
+end
+
+def run_spinach_tests(tags)
+ #run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!')
+
+ success = run_spinach_command(%W(--tags #{tags}))
+ 3.times do |_|
+ break if success
+ break unless File.exists?('tmp/spinach-rerun.txt')
+
+ tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
+ puts ''
+ puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red
+ puts ''
+ sleep(3)
+ success = run_spinach_command(tests)
end
+
+ raise("spinach tests for #{tags} failed!") unless success
end
diff --git a/lib/version_check.rb b/lib/version_check.rb
index ea23344948c..91ad07feee5 100644
--- a/lib/version_check.rb
+++ b/lib/version_check.rb
@@ -13,6 +13,6 @@ class VersionCheck
end
def host
- 'https://version.gitlab.com/check.png'
+ 'https://version.gitlab.com/check.svg'
end
end
diff --git a/public/404.html b/public/404.html
index a0106bc760d..4862770cc2a 100644
--- a/public/404.html
+++ b/public/404.html
@@ -2,11 +2,51 @@
<html>
<head>
<title>The page you're looking for could not be found (404)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
- <h1>404</h1>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ 404
+ </h1>
<h3>The page you're looking for could not be found.</h3>
<hr/>
<p>Make sure the address is correct and that the page hasn't moved.</p>
diff --git a/public/422.html b/public/422.html
index 026997b48e3..055b0bde165 100644
--- a/public/422.html
+++ b/public/422.html
@@ -2,12 +2,51 @@
<html>
<head>
<title>The change you requested was rejected (422)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
- <!-- This file lives in public/422.html -->
- <h1>422</h1>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ 422
+ </h1>
<h3>The change you requested was rejected.</h3>
<hr />
<p>Make sure you have access to the thing you tried to change.</p>
diff --git a/public/500.html b/public/500.html
index 08c11bbd05a..3d59d1392f5 100644
--- a/public/500.html
+++ b/public/500.html
@@ -2,10 +2,50 @@
<html>
<head>
<title>Something went wrong (500)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
- <h1>500</h1>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ 500
+ </h1>
<h3>Whoops, something went wrong on our end.</h3>
<hr/>
<p>Try refreshing the page, or going back and attempting the action again.</p>
diff --git a/public/502.html b/public/502.html
index 9480a928439..67dfd8a2743 100644
--- a/public/502.html
+++ b/public/502.html
@@ -2,10 +2,50 @@
<html>
<head>
<title>GitLab is not responding (502)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
- <h1>502</h1>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ 502
+ </h1>
<h3>Whoops, GitLab is taking too much time to respond.</h3>
<hr/>
<p>Try refreshing the page, or going back and attempting the action again.</p>
diff --git a/public/deploy.html b/public/deploy.html
index 3822ed4b64d..48976dacf41 100644
--- a/public/deploy.html
+++ b/public/deploy.html
@@ -2,12 +2,49 @@
<html>
<head>
<title>Deploy in progress</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 0;
+ width: 800px;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+ </style>
</head>
<body>
<h1>
- <img src="/logo.svg" /><br />
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
Deploy in progress
</h1>
<h3>Please try again in a few minutes.</h3>
diff --git a/public/static.css b/public/static.css
deleted file mode 100644
index 0a2b6060d48..00000000000
--- a/public/static.css
+++ /dev/null
@@ -1,36 +0,0 @@
-body {
- color: #666;
- text-align: center;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
- width: 800px;
- margin: auto;
- font-size: 14px;
-}
-
-h1 {
- font-size: 56px;
- line-height: 100px;
- font-weight: normal;
- color: #456;
-}
-
-h2 {
- font-size: 24px;
- color: #666;
- line-height: 1.5em;
-}
-
-h3 {
- color: #456;
- font-size: 20px;
- font-weight: normal;
- line-height: 28px;
-}
-
-hr {
- margin: 18px 0;
- border: 0;
- border-top: 1px solid #EEE;
- border-bottom: 1px solid white;
-}
diff --git a/scripts/ci/prepare_build.sh b/scripts/ci/prepare_build.sh
deleted file mode 100755
index 864a683a1bd..00000000000
--- a/scripts/ci/prepare_build.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-if [ -f /.dockerinit ]; then
- export FLAGS=(--deployment --path /cache)
-
- apt-get update -qq
- apt-get install -y -qq nodejs
-
- wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb
- dpkg -i phantomjs_1.9.0-1+b1_amd64.deb
-
- cp config/database.yml.mysql config/database.yml
- sed -i "s/username:.*/username: root/g" config/database.yml
- sed -i "s/password:.*/password:/g" config/database.yml
- sed -i "s/# socket:.*/host: mysql/g" config/database.yml
-else
- export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
-
- cp config/database.yml.mysql config/database.yml
- sed -i "s/username\:.*$/username\: runner/" config/database.yml
- sed -i "s/password\:.*$/password\: 'password'/" config/database.yml
- sed -i "s/gitlab_ci_test/gitlab_ci_test_$((RANDOM/5000))/" config/database.yml
-fi
diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh
new file mode 100755
index 00000000000..0a4239e132c
--- /dev/null
+++ b/scripts/notify_slack.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+# Sends Slack notification ERROR_MSG to CHANNEL
+# An env. variable CI_SLACK_WEBHOOK_URL needs to be set.
+
+CHANNEL=$1
+ERROR_MSG=$2
+
+if [ -z "$CHANNEL" ] || [ -z "$ERROR_MSG" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ]; then
+ echo "Missing argument(s) - Use: $0 channel message"
+ echo "and set CI_SLACK_WEBHOOK_URL environment variable."
+else
+ curl -X POST --data-urlencode 'payload={"channel": "'"$CHANNEL"'", "username": "gitlab-ci", "text": "'"$ERROR_MSG"'", "icon_emoji": ":gitlab:"}' "$CI_SLACK_WEBHOOK_URL"
+fi \ No newline at end of file
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 119cc90fc1e..4a7ee7dbb64 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,10 +1,30 @@
#!/bin/bash
+
+retry() {
+ for i in $(seq 1 3); do
+ if eval "$@"; then
+ return 0
+ fi
+ sleep 3s
+ echo "Retrying..."
+ done
+ return 1
+}
+
if [ -f /.dockerinit ]; then
- wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb
+ mkdir -p vendor
+
+ # Install phantomjs package
+ pushd vendor
+ if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then
+ wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb
+ fi
dpkg -i phantomjs_1.9.8-0jessie_amd64.deb
+ popd
- apt-get update -qq
- apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client
+ # Try to install packages
+ retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
+ libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip'
cp config/database.yml.mysql config/database.yml
sed -i 's/username:.*/username: root/g' config/database.yml
@@ -13,8 +33,8 @@ if [ -f /.dockerinit ]; then
cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml
- FLAGS=(--deployment --path /cache)
- export FLAGS
+
+ export FLAGS=(--path vendor --retry 3)
else
export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
cp config/database.yml.mysql config/database.yml
diff --git a/spec/benchmarks/finders/issues_finder_spec.rb b/spec/benchmarks/finders/issues_finder_spec.rb
deleted file mode 100644
index b57a33004a4..00000000000
--- a/spec/benchmarks/finders/issues_finder_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require 'spec_helper'
-
-describe IssuesFinder, benchmark: true do
- describe '#execute' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
-
- let(:label1) { create(:label, project: project, title: 'A') }
- let(:label2) { create(:label, project: project, title: 'B') }
-
- before do
- 10.times do |n|
- issue = create(:issue, author: user, project: project)
-
- if n > 4
- create(:label_link, label: label1, target: issue)
- create(:label_link, label: label2, target: issue)
- end
- end
- end
-
- describe 'retrieving issues without labels' do
- let(:finder) do
- IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
- state: 'opened')
- end
-
- benchmark_subject { finder.execute }
-
- it { is_expected.to iterate_per_second(2000) }
- end
-
- describe 'retrieving issues with labels' do
- let(:finder) do
- IssuesFinder.new(user, scope: 'all', label_name: label1.title,
- state: 'opened')
- end
-
- benchmark_subject { finder.execute }
-
- it { is_expected.to iterate_per_second(1000) }
- end
-
- describe 'retrieving issues for a single project' do
- let(:finder) do
- IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
- state: 'opened', project_id: project.id)
- end
-
- benchmark_subject { finder.execute }
-
- it { is_expected.to iterate_per_second(2000) }
- end
- end
-end
diff --git a/spec/benchmarks/finders/trending_projects_finder_spec.rb b/spec/benchmarks/finders/trending_projects_finder_spec.rb
deleted file mode 100644
index 551ce21840d..00000000000
--- a/spec/benchmarks/finders/trending_projects_finder_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'spec_helper'
-
-describe TrendingProjectsFinder, benchmark: true do
- describe '#execute' do
- let(:finder) { described_class.new }
- let(:user) { create(:user) }
-
- # to_a is used to force actually running the query (instead of just building
- # it).
- benchmark_subject { finder.execute(user).non_archived.to_a }
-
- it { is_expected.to iterate_per_second(500) }
- end
-end
diff --git a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
deleted file mode 100644
index 3855763b200..00000000000
--- a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::ReferenceFilter, benchmark: true do
- let(:input) do
- html = <<-EOF
-<p>Hello @alice and @bob, how are you doing today?</p>
-<p>This is simple @dummy text to see how the @ReferenceFilter class performs
-when @processing HTML.</p>
- EOF
-
- Nokogiri::HTML.fragment(html)
- end
-
- let(:project) { create(:empty_project) }
-
- let(:filter) { described_class.new(input, project: project) }
-
- describe '#replace_text_nodes_matching' do
- let(:iterations) { 6000 }
-
- describe 'with identical input and output HTML' do
- benchmark_subject do
- filter.replace_text_nodes_matching(User.reference_pattern) do |content|
- content
- end
- end
-
- it { is_expected.to iterate_per_second(iterations) }
- end
-
- describe 'with different input and output HTML' do
- benchmark_subject do
- filter.replace_text_nodes_matching(User.reference_pattern) do |content|
- '@eve'
- end
- end
-
- it { is_expected.to iterate_per_second(iterations) }
- end
- end
-end
diff --git a/spec/benchmarks/models/milestone_spec.rb b/spec/benchmarks/models/milestone_spec.rb
deleted file mode 100644
index a94afc4c40d..00000000000
--- a/spec/benchmarks/models/milestone_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'spec_helper'
-
-describe Milestone, benchmark: true do
- describe '#sort_issues' do
- let(:milestone) { create(:milestone) }
-
- let(:issue1) { create(:issue, milestone: milestone) }
- let(:issue2) { create(:issue, milestone: milestone) }
- let(:issue3) { create(:issue, milestone: milestone) }
-
- let(:issue_ids) { [issue3.id, issue2.id, issue1.id] }
-
- benchmark_subject { milestone.sort_issues(issue_ids) }
-
- it { is_expected.to iterate_per_second(500) }
- end
-end
diff --git a/spec/benchmarks/models/project_spec.rb b/spec/benchmarks/models/project_spec.rb
deleted file mode 100644
index cee0949edc5..00000000000
--- a/spec/benchmarks/models/project_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'spec_helper'
-
-describe Project, benchmark: true do
- describe '.trending' do
- let(:group) { create(:group) }
- let(:project1) { create(:empty_project, :public, group: group) }
- let(:project2) { create(:empty_project, :public, group: group) }
-
- let(:iterations) { 500 }
-
- before do
- 2.times do
- create(:note_on_commit, project: project1)
- end
-
- create(:note_on_commit, project: project2)
- end
-
- describe 'without an explicit start date' do
- benchmark_subject { described_class.trending.to_a }
-
- it { is_expected.to iterate_per_second(iterations) }
- end
-
- describe 'with an explicit start date' do
- let(:date) { 1.month.ago }
-
- benchmark_subject { described_class.trending(date).to_a }
-
- it { is_expected.to iterate_per_second(iterations) }
- end
- end
-
- describe '.find_with_namespace' do
- let(:group) { create(:group, name: 'sisinmaru') }
- let(:project) { create(:project, name: 'maru', namespace: group) }
-
- describe 'using a capitalized namespace' do
- benchmark_subject { described_class.find_with_namespace('sisinmaru/MARU') }
-
- it { is_expected.to iterate_per_second(600) }
- end
-
- describe 'using a lowercased namespace' do
- benchmark_subject { described_class.find_with_namespace('sisinmaru/maru') }
-
- it { is_expected.to iterate_per_second(600) }
- end
- end
-end
diff --git a/spec/benchmarks/models/project_team_spec.rb b/spec/benchmarks/models/project_team_spec.rb
deleted file mode 100644
index 8b039ef7317..00000000000
--- a/spec/benchmarks/models/project_team_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-require 'spec_helper'
-
-describe ProjectTeam, benchmark: true do
- describe '#max_member_access' do
- let(:group) { create(:group) }
- let(:project) { create(:empty_project, group: group) }
- let(:user) { create(:user) }
-
- before do
- project.team << [user, :master]
-
- 5.times do
- project.team << [create(:user), :reporter]
-
- project.group.add_user(create(:user), :reporter)
- end
- end
-
- benchmark_subject { project.team.max_member_access(user.id) }
-
- it { is_expected.to iterate_per_second(35000) }
- end
-end
diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb
deleted file mode 100644
index 1be7a8d3ed9..00000000000
--- a/spec/benchmarks/models/user_spec.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-require 'spec_helper'
-
-describe User, benchmark: true do
- describe '.all' do
- before do
- 10.times { create(:user) }
- end
-
- benchmark_subject { User.all.to_a }
-
- it { is_expected.to iterate_per_second(500) }
- end
-
- describe '.by_login' do
- before do
- %w{Alice Bob Eve}.each do |name|
- create(:user,
- email: "#{name}@gitlab.com",
- username: name,
- name: name)
- end
- end
-
- # The iteration count is based on the query taking little over 1 ms when
- # using PostgreSQL.
- let(:iterations) { 900 }
-
- describe 'using a capitalized username' do
- benchmark_subject { User.by_login('Alice') }
-
- it { is_expected.to iterate_per_second(iterations) }
- end
-
- describe 'using a lowercase username' do
- benchmark_subject { User.by_login('alice') }
-
- it { is_expected.to iterate_per_second(iterations) }
- end
-
- describe 'using a capitalized Email address' do
- benchmark_subject { User.by_login('Alice@gitlab.com') }
-
- it { is_expected.to iterate_per_second(iterations) }
- end
-
- describe 'using a lowercase Email address' do
- benchmark_subject { User.by_login('alice@gitlab.com') }
-
- it { is_expected.to iterate_per_second(iterations) }
- end
- end
-
- describe '.find_by_any_email' do
- let(:user) { create(:user) }
-
- describe 'using a user with only a single Email address' do
- let(:email) { user.email }
-
- benchmark_subject { User.find_by_any_email(email) }
-
- it { is_expected.to iterate_per_second(1000) }
- end
-
- describe 'using a user with multiple Email addresses' do
- let(:email) { user.emails.first.email }
-
- benchmark_subject { User.find_by_any_email(email) }
-
- before do
- 10.times do
- user.emails.create(email: FFaker::Internet.email)
- end
- end
-
- it { is_expected.to iterate_per_second(1000) }
- end
- end
-end
diff --git a/spec/benchmarks/services/projects/create_service_spec.rb b/spec/benchmarks/services/projects/create_service_spec.rb
deleted file mode 100644
index 25ed48c34fd..00000000000
--- a/spec/benchmarks/services/projects/create_service_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'spec_helper'
-
-describe Projects::CreateService, benchmark: true do
- describe '#execute' do
- let(:user) { create(:user, :admin) }
-
- let(:group) do
- group = create(:group)
-
- create(:group_member, group: group, user: user)
-
- group
- end
-
- benchmark_subject do
- name = SecureRandom.hex
- service = described_class.new(user,
- name: name,
- path: name,
- namespace_id: group.id,
- visibility_level: Gitlab::VisibilityLevel::PUBLIC)
-
- service.execute
- end
-
- it { is_expected.to iterate_per_second(0.5) }
- end
-end
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
new file mode 100644
index 00000000000..462afb24f08
--- /dev/null
+++ b/spec/config/mail_room_spec.rb
@@ -0,0 +1,56 @@
+require "spec_helper"
+
+describe "mail_room.yml" do
+ let(:config_path) { "config/mail_room.yml" }
+ let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
+
+ context "when incoming email is disabled" do
+ before do
+ ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_disabled.yml").to_s
+ end
+
+ after do
+ ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
+ end
+
+ it "contains no configuration" do
+ expect(configuration[:mailboxes]).to be_nil
+ end
+ end
+
+ context "when incoming email is enabled" do
+ before do
+ ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_enabled.yml").to_s
+ end
+
+ after do
+ ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
+ end
+
+ it "contains the intended configuration" do
+ expect(configuration[:mailboxes].length).to eq(1)
+
+ mailbox = configuration[:mailboxes].first
+
+ expect(mailbox[:host]).to eq("imap.gmail.com")
+ expect(mailbox[:port]).to eq(993)
+ expect(mailbox[:ssl]).to eq(true)
+ expect(mailbox[:start_tls]).to eq(false)
+ expect(mailbox[:email]).to eq("gitlab-incoming@gmail.com")
+ expect(mailbox[:password]).to eq("[REDACTED]")
+ expect(mailbox[:name]).to eq("inbox")
+
+ redis_config_file = Rails.root.join('config', 'resque.yml')
+
+ redis_url =
+ if File.exists?(redis_config_file)
+ YAML.load_file(redis_config_file)[Rails.env]
+ else
+ "redis://localhost:6379"
+ end
+
+ expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url)
+ expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url)
+ end
+ end
+end
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
index 15824a1c67f..80a418feb3e 100644
--- a/spec/controllers/abuse_reports_controller_spec.rb
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -1,76 +1,46 @@
require 'spec_helper'
describe AbuseReportsController do
- let(:reporter) { create(:user) }
- let(:user) { create(:user) }
- let(:message) { "This user is a spammer" }
+ let(:reporter) { create(:user) }
+ let(:user) { create(:user) }
+ let(:attrs) do
+ attributes_for(:abuse_report) do |hash|
+ hash[:user_id] = user.id
+ end
+ end
before do
sign_in(reporter)
end
- describe "POST create" do
- context "with admin notification email set" do
- let(:admin_email) { "admin@example.com"}
-
- before(:each) do
- stub_application_setting(admin_notification_email: admin_email)
+ describe 'POST create' do
+ context 'with valid attributes' do
+ it 'saves the abuse report' do
+ expect do
+ post :create, abuse_report: attrs
+ end.to change { AbuseReport.count }.by(1)
end
- it "sends a notification email" do
- perform_enqueued_jobs do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
-
- email = ActionMailer::Base.deliveries.last
+ it 'calls notify' do
+ expect_any_instance_of(AbuseReport).to receive(:notify)
- expect(email.to).to eq([admin_email])
- expect(email.subject).to include(user.username)
- expect(email.text_part.body).to include(message)
- end
+ post :create, abuse_report: attrs
end
- it "saves the abuse report" do
- perform_enqueued_jobs do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.to change { AbuseReport.count }.by(1)
- end
- end
- end
+ it 'redirects back to the reported user' do
+ post :create, abuse_report: attrs
- context "without admin notification email set" do
- before(:each) do
- stub_application_setting(admin_notification_email: nil)
+ expect(response).to redirect_to user
end
+ end
- it "does not send a notification email" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.not_to change { ActionMailer::Base.deliveries.count }
- end
+ context 'with invalid attributes' do
+ it 'renders new' do
+ attrs.delete(:user_id)
+ post :create, abuse_report: attrs
- it "saves the abuse report" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.to change { AbuseReport.count }.by(1)
+ expect(response).to render_template(:new)
end
end
end
-
end
diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb
new file mode 100644
index 00000000000..c131d22a30a
--- /dev/null
+++ b/spec/controllers/admin/identities_controller_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Admin::IdentitiesController do
+ let(:admin) { create(:admin) }
+ before { sign_in(admin) }
+
+ describe 'UPDATE identity' do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
+
+ it 'repairs ldap blocks' do
+ expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute)
+
+ put :update, user_id: user.username, id: user.ldap_identity.id, identity: { provider: 'twitter' }
+ end
+ end
+
+ describe 'DELETE identity' do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
+
+ it 'repairs ldap blocks' do
+ expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute)
+
+ delete :destroy, user_id: user.username, id: user.ldap_identity.id
+ end
+ end
+end
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
new file mode 100644
index 00000000000..b51b303a714
--- /dev/null
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Admin::SpamLogsController do
+ let(:admin) { create(:admin) }
+ let(:user) { create(:user) }
+ let!(:first_spam) { create(:spam_log, user: user) }
+ let!(:second_spam) { create(:spam_log, user: user) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe '#index' do
+ it 'lists all spam logs' do
+ get :index
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ describe '#destroy' do
+ it 'removes only the spam log when removing log' do
+ expect { delete :destroy, id: first_spam.id }.to change { SpamLog.count }.by(-1)
+ expect(User.find(user.id)).to be_truthy
+ expect(response.status).to eq(200)
+ end
+
+ it 'removes user and his spam logs when removing the user' do
+ delete :destroy, id: first_spam.id, remove_user: true
+
+ expect(flash[:notice]).to eq "User #{user.username} was successfully removed."
+ expect(response.status).to eq(302)
+ expect(SpamLog.count).to eq(0)
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 8b7af4d3a0a..5b1f65d7aff 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -34,17 +34,34 @@ describe Admin::UsersController do
end
describe 'PUT unblock/:id' do
- let(:user) { create(:user) }
-
- before do
- user.block
+ context 'ldap blocked users' do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain') }
+
+ before do
+ user.ldap_block
+ end
+
+ it 'will not unblock user' do
+ put :unblock, id: user.username
+ user.reload
+ expect(user.blocked?).to be_truthy
+ expect(flash[:alert]).to eq 'This user cannot be unlocked manually from GitLab'
+ end
end
- it 'unblocks user' do
- put :unblock, id: user.username
- user.reload
- expect(user.blocked?).to be_falsey
- expect(flash[:notice]).to eq 'Successfully unblocked'
+ context 'manually blocked users' do
+ let(:user) { create(:user) }
+
+ before do
+ user.block
+ end
+
+ it 'unblocks user' do
+ put :unblock, id: user.username
+ user.reload
+ expect(user.blocked?).to be_falsey
+ expect(flash[:notice]).to eq 'Successfully unblocked'
+ end
end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 85379a8e984..410b993fdfb 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -21,7 +21,7 @@ describe AutocompleteController do
it { expect(body).to be_kind_of(Array) }
it { expect(body.size).to eq 1 }
- it { expect(body.first["username"]).to eq user.username }
+ it { expect(body.map { |u| u["username"] }).to include(user.username) }
end
describe 'GET #users with unknown project' do
diff --git a/spec/controllers/blame_controller_spec.rb b/spec/controllers/blame_controller_spec.rb
deleted file mode 100644
index 3ad4d5fc0a8..00000000000
--- a/spec/controllers/blame_controller_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'spec_helper'
-
-describe Projects::BlameController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- project.team << [user, :master]
- controller.instance_variable_set(:@project, project)
- end
-
- describe "GET show" do
- render_views
-
- before do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: id)
- end
-
- context "valid file" do
- let(:id) { 'master/files/ruby/popen.rb' }
- it { is_expected.to respond_with(:success) }
-
- it 'groups blames properly' do
- blame = assigns(:blame)
- # Sanity check a few items
- expect(blame.count).to eq(18)
- expect(blame[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e')
- expect(blame[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""])
-
- expect(blame[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e')
- expect(blame[1][:lines]).to eq(["module Popen", " extend self"])
-
- expect(blame[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e')
- expect(blame[-1][:lines]).to eq([" end", "end"])
- end
- end
- end
-end
diff --git a/spec/controllers/branches_controller_spec.rb b/spec/controllers/branches_controller_spec.rb
deleted file mode 100644
index 8e06d4bdc77..00000000000
--- a/spec/controllers/branches_controller_spec.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-require 'spec_helper'
-
-describe Projects::BranchesController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- project.team << [user, :master]
-
- allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
- allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0'])
- controller.instance_variable_set(:@project, project)
- end
-
- describe "POST create" do
- render_views
-
- before do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- ref: ref
- end
-
- context "valid branch name, valid source" do
- let(:branch) { "merge_branch" }
- let(:ref) { "master" }
- it 'redirects' do
- expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/merge_branch")
- end
- end
-
- context "invalid branch name, valid ref" do
- let(:branch) { "<script>alert('merge');</script>" }
- let(:ref) { "master" }
- it 'redirects' do
- expect(subject).
- to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');")
- end
- end
-
- context "valid branch name, invalid ref" do
- let(:branch) { "merge_branch" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- end
-
- context "invalid branch name, invalid ref" do
- let(:branch) { "<script>alert('merge');</script>" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- end
-
- context "valid branch name with encoded slashes" do
- let(:branch) { "feature%2Ftest" }
- let(:ref) { "<script>alert('ref');</script>" }
- it { is_expected.to render_template('new') }
- it { project.repository.branch_names.include?('feature/test')}
- end
- end
-
- describe "POST destroy" do
- render_views
-
- before do
- post :destroy,
- format: :js,
- id: branch,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
- end
-
- context "valid branch name, valid source" do
- let(:branch) { "feature" }
-
- it { expect(response.status).to eq(200) }
- it { expect(subject).to render_template('destroy') }
- end
-
- context "valid branch name with unencoded slashes" do
- let(:branch) { "improve/awesome" }
-
- it { expect(response.status).to eq(200) }
- it { expect(subject).to render_template('destroy') }
- end
-
- context "valid branch name with encoded slashes" do
- let(:branch) { "improve%2Fawesome" }
-
- it { expect(response.status).to eq(200) }
- it { expect(subject).to render_template('destroy') }
- end
- context "invalid branch name, valid ref" do
- let(:branch) { "no-branch" }
-
- it { expect(response.status).to eq(404) }
- it { expect(subject).to render_template('destroy') }
- end
- end
-end
diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb
new file mode 100644
index 00000000000..db0748f323f
--- /dev/null
+++ b/spec/controllers/ci/projects_controller_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Ci::ProjectsController do
+ let(:visibility) { :public }
+ let!(:project) { create(:project, visibility, ci_id: 1) }
+ let(:ci_id) { project.ci_id }
+
+ ##
+ # Specs for *deprecated* CI badge
+ #
+ describe '#badge' do
+ shared_examples 'badge provider' do
+ it 'shows badge' do
+ expect(response.status).to eq 200
+ expect(response.headers)
+ .to include('Content-Type' => 'image/svg+xml')
+ end
+ end
+
+ context 'user not signed in' do
+ before { get(:badge, id: ci_id) }
+
+ context 'project has no ci_id reference' do
+ let(:ci_id) { 123 }
+
+ it 'returns 404' do
+ expect(response.status).to eq 404
+ end
+ end
+
+ context 'project is public' do
+ let(:visibility) { :public }
+ it_behaves_like 'badge provider'
+ end
+
+ context 'project is private' do
+ let(:visibility) { :private }
+ it_behaves_like 'badge provider'
+ end
+ end
+
+ context 'user signed in' do
+ let(:user) { create(:user) }
+ before { sign_in(user) }
+ before { get(:badge, id: ci_id) }
+
+ context 'private is internal' do
+ let(:visibility) { :internal }
+ it_behaves_like 'badge provider'
+ end
+ end
+ end
+end
diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
index 7793bf1e421..f09e4fcb154 100644
--- a/spec/controllers/commit_controller_spec.rb
+++ b/spec/controllers/commit_controller_spec.rb
@@ -81,7 +81,7 @@ describe Projects::CommitController do
expect(response.body).to start_with("diff --git")
# without whitespace option, there are more than 2 diff_splits
- diff_splits = assigns(:diffs)[0].diff.split("\n")
+ diff_splits = assigns(:diffs).first.diff.split("\n")
expect(diff_splits.length).to be <= 2
end
end
@@ -143,4 +143,53 @@ describe Projects::CommitController do
expect(assigns(:tags)).to include("v1.1.0")
end
end
+
+ describe '#revert' do
+ context 'when target branch is not provided' do
+ it 'should render the 404 page' do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: commit.id)
+
+ expect(response).not_to be_success
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when the revert was successful' do
+ it 'should redirect to the commits page' do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+
+ expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
+ expect(flash[:notice]).to eq('The commit has been successfully reverted.')
+ end
+ end
+
+ context 'when the revert failed' do
+ before do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+ end
+
+ it 'should redirect to the commit page' do
+ # Reverting a commit that has been already reverted.
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+
+ expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
+ expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.')
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
new file mode 100644
index 00000000000..938e97298b6
--- /dev/null
+++ b/spec/controllers/groups_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+describe GroupsController do
+ describe 'GET index' do
+ context 'as a user' do
+ it 'redirects to Groups Dashboard' do
+ sign_in(create(:user))
+
+ get :index
+
+ expect(response).to redirect_to(dashboard_groups_path)
+ end
+ end
+
+ context 'as a guest' do
+ it 'redirects to Explore Groups' do
+ get :index
+
+ expect(response).to redirect_to(explore_groups_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/profile_keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index b6573f105dc..b6573f105dc 100644
--- a/spec/controllers/profile_keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
new file mode 100644
index 00000000000..25f06299a29
--- /dev/null
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Projects::BlameController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ project.team << [user, :master]
+ controller.instance_variable_set(:@project, project)
+ end
+
+ describe "GET show" do
+ render_views
+
+ before do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id)
+ end
+
+ context "valid file" do
+ let(:id) { 'master/files/ruby/popen.rb' }
+ it { is_expected.to respond_with(:success) }
+ end
+ end
+end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
new file mode 100644
index 00000000000..98ae424ed7c
--- /dev/null
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe Projects::BranchesController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ project.team << [user, :master]
+
+ allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
+ allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0'])
+ controller.instance_variable_set(:@project, project)
+ end
+
+ describe "POST create" do
+ render_views
+
+ context "on creation of a new branch" do
+ before do
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ ref: ref
+ end
+
+ context "valid branch name, valid source" do
+ let(:branch) { "merge_branch" }
+ let(:ref) { "master" }
+ it 'redirects' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/merge_branch")
+ end
+ end
+
+ context "invalid branch name, valid ref" do
+ let(:branch) { "<script>alert('merge');</script>" }
+ let(:ref) { "master" }
+ it 'redirects' do
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');")
+ end
+ end
+
+ context "valid branch name, invalid ref" do
+ let(:branch) { "merge_branch" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ end
+
+ context "invalid branch name, invalid ref" do
+ let(:branch) { "<script>alert('merge');</script>" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ end
+
+ context "valid branch name with encoded slashes" do
+ let(:branch) { "feature%2Ftest" }
+ let(:ref) { "<script>alert('ref');</script>" }
+ it { is_expected.to render_template('new') }
+ it { project.repository.branch_names.include?('feature/test') }
+ end
+ end
+
+ describe "created from the new branch button on issues" do
+ let(:branch) { "1-feature-branch" }
+ let!(:issue) { create(:issue, project: project) }
+
+
+ it 'redirects' do
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
+ expect(subject).
+ to redirect_to("/#{project.path_with_namespace}/tree/1-feature-branch")
+ end
+
+ it 'posts a system note' do
+ expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch")
+
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+ end
+
+ end
+ end
+
+ describe "POST destroy" do
+ render_views
+
+ before do
+ post :destroy,
+ format: :js,
+ id: branch,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+
+ context "valid branch name, valid source" do
+ let(:branch) { "feature" }
+
+ it { expect(response.status).to eq(200) }
+ it { expect(subject).to render_template('destroy') }
+ end
+
+ context "valid branch name with unencoded slashes" do
+ let(:branch) { "improve/awesome" }
+
+ it { expect(response.status).to eq(200) }
+ it { expect(subject).to render_template('destroy') }
+ end
+
+ context "valid branch name with encoded slashes" do
+ let(:branch) { "improve%2Fawesome" }
+
+ it { expect(response.status).to eq(200) }
+ it { expect(subject).to render_template('destroy') }
+ end
+ context "invalid branch name, valid ref" do
+ let(:branch) { "no-branch" }
+
+ it { expect(response.status).to eq(404) }
+ it { expect(subject).to render_template('destroy') }
+ end
+ end
+end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
new file mode 100644
index 00000000000..438e776ec4b
--- /dev/null
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+describe Projects::CommitController do
+ describe 'GET show' do
+ let(:project) { create(:project) }
+
+ before do
+ user = create(:user)
+ project.team << [user, :master]
+
+ sign_in(user)
+ end
+
+ context 'with valid id' do
+ it 'responds with 200' do
+ go id: project.commit.id
+
+ expect(response).to be_ok
+ end
+ end
+
+ context 'with invalid id' do
+ it 'responds with 404' do
+ go id: project.commit.id.reverse
+
+ expect(response).to be_not_found
+ end
+ end
+
+ def go(id:)
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id
+ end
+ end
+end
diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 7d8089c4bc6..7d8089c4bc6 100644
--- a/spec/controllers/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index be19f1abc53..788a609ee40 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::CompareController do
to: ref_to)
expect(response).to be_success
- expect(assigns(:diffs).length).to be >= 1
+ expect(assigns(:diffs).first).to_not be_nil
expect(assigns(:commits).length).to be >= 1
end
@@ -32,10 +32,10 @@ describe Projects::CompareController do
w: 1)
expect(response).to be_success
- expect(assigns(:diffs).length).to be >= 1
+ expect(assigns(:diffs).first).to_not be_nil
expect(assigns(:commits).length).to be >= 1
# without whitespace option, there are more than 2 diff_splits
- diff_splits = assigns(:diffs)[0].diff.split("\n")
+ diff_splits = assigns(:diffs).first.diff.split("\n")
expect(diff_splits.length).to be <= 2
end
@@ -48,7 +48,7 @@ describe Projects::CompareController do
to: ref_to)
expect(response).to be_success
- expect(assigns(:diffs)).to eq([])
+ expect(assigns(:diffs).to_a).to eq([])
expect(assigns(:commits)).to eq([])
end
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
new file mode 100644
index 00000000000..038dfeb8466
--- /dev/null
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Projects::FindFileController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ project.team << [user, :master]
+ controller.instance_variable_set(:@project, project)
+ end
+
+ describe "GET #show" do
+ # Make sure any errors accessing the tree in our views bubble up to this spec
+ render_views
+
+ before do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id)
+ end
+
+ context "valid branch" do
+ let(:id) { 'master' }
+ it { is_expected.to respond_with(:success) }
+ end
+
+ context "invalid branch" do
+ let(:id) { 'invalid-branch' }
+ it { is_expected.to respond_with(:not_found) }
+ end
+ end
+
+ describe "GET #list" do
+ def go(format: 'json')
+ get :list,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id,
+ format: format
+ end
+
+ context "valid branch" do
+ let(:id) { 'master' }
+ it 'returns an array of file path list' do
+ go
+
+ json = JSON.parse(response.body)
+ is_expected.to respond_with(:success)
+ expect(json).not_to eq(nil)
+ expect(json.length).to be >= 0
+ end
+ end
+
+ context "invalid branch" do
+ let(:id) { 'invalid-branch' }
+
+ it 'responds with status 404' do
+ go
+ is_expected.to respond_with(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
new file mode 100644
index 00000000000..70ed8f3a62e
--- /dev/null
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Projects::ForksController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let(:group) { create(:group, owner: forked_project.creator) }
+
+ describe 'GET index' do
+ def get_forks
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+
+ context 'when fork is public' do
+ before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) }
+
+ it 'should be visible for non logged in users' do
+ get_forks
+
+ expect(assigns[:forks]).to be_present
+ end
+ end
+
+ context 'when fork is private' do
+ before do
+ forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group)
+ end
+
+ it 'should not be visible for non logged in users' do
+ get_forks
+
+ expect(assigns[:forks]).to be_blank
+ end
+
+ context 'when user is logged in' do
+ before { sign_in(project.creator) }
+
+ context 'when user is not a Project member neither a group member' do
+ it 'should not see the Project listed' do
+ get_forks
+
+ expect(assigns[:forks]).to be_blank
+ end
+ end
+
+ context 'when user is a member of the Project' do
+ before { forked_project.team << [project.creator, :developer] }
+
+ it 'should see the project listed' do
+ get_forks
+
+ expect(assigns[:forks]).to be_present
+ end
+ end
+
+ context 'when user is a member of the Group' do
+ before { forked_project.group.add_developer(project.creator) }
+
+ it 'should see the project listed' do
+ get_forks
+
+ expect(assigns[:forks]).to be_present
+ end
+ end
+
+ end
+ end
+ end
+
+end
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
new file mode 100644
index 00000000000..2acbba469e3
--- /dev/null
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -0,0 +1,121 @@
+require 'spec_helper'
+
+describe Projects::ImportsController do
+ let(:user) { create(:user) }
+
+ describe 'GET #show' do
+ context 'when repository does not exists' do
+ let(:project) { create(:empty_project) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ it 'renders template' do
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ expect(response).to render_template :show
+ end
+
+ it 'sets flash.now if params is present' do
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' }
+
+ expect(flash.now[:notice]).to eq 'Started'
+ end
+ end
+
+ context 'when repository exists' do
+ let(:project) { create(:project_empty_repo, import_url: 'https://github.com/vim/vim.git') }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ context 'when import is in progress' do
+ before do
+ project.update_attribute(:import_status, :started)
+ end
+
+ it 'renders template' do
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ expect(response).to render_template :show
+ end
+
+ it 'sets flash.now if params is present' do
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' }
+
+ expect(flash.now[:notice]).to eq 'In progress'
+ end
+ end
+
+ context 'when import failed' do
+ before do
+ project.update_attribute(:import_status, :failed)
+ end
+
+ it 'redirects to new_namespace_project_import_path' do
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ expect(response).to redirect_to new_namespace_project_import_path(project.namespace, project)
+ end
+ end
+
+ context 'when import finished' do
+ before do
+ project.update_attribute(:import_status, :finished)
+ end
+
+ context 'when project is a fork' do
+ it 'redirects to namespace_project_path' do
+ allow_any_instance_of(Project).to receive(:forked?).and_return(true)
+
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ expect(flash[:notice]).to eq 'The project was successfully forked.'
+ expect(response).to redirect_to namespace_project_path(project.namespace, project)
+ end
+ end
+
+ context 'when project is external' do
+ it 'redirects to namespace_project_path' do
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ expect(flash[:notice]).to eq 'The project was successfully imported.'
+ expect(response).to redirect_to namespace_project_path(project.namespace, project)
+ end
+ end
+
+ context 'when continue params is present' do
+ let(:params) do
+ {
+ to: namespace_project_path(project.namespace, project),
+ notice: 'Finished'
+ }
+ end
+
+ it 'redirects to params[:to]' do
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: params
+
+ expect(flash[:notice]).to eq params[:notice]
+ expect(response).to redirect_to params[:to]
+ end
+ end
+ end
+
+ context 'when import never happened' do
+ before do
+ project.update_attribute(:import_status, :none)
+ end
+
+ it 'redirects to namespace_project_path' do
+ get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ expect(response).to redirect_to namespace_project_path(project.namespace, project)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 76d56bc989d..2cd81231144 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(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
+ describe "GET #index" do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
- describe "GET #index" do
it "returns index" do
get :index, namespace_id: project.namespace.path, project_id: project.path
@@ -38,6 +38,152 @@ 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(:empty_project, :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
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 6aaec224f6e..e82fe26c7a6 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -123,6 +123,40 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET #index' do
+ def get_merge_requests
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ state: 'opened'
+ end
+
+ context 'when filtering by opened state' do
+
+ context 'with opened merge requests' do
+ it 'should list those merge requests' do
+ get_merge_requests
+
+ expect(assigns(:merge_requests)).to include(merge_request)
+ end
+ end
+
+ context 'with reopened merge requests' do
+ before do
+ merge_request.close!
+ merge_request.reopen!
+ end
+
+ it 'should list those merge requests' do
+ get_merge_requests
+
+ expect(assigns(:merge_requests)).to include(merge_request)
+ end
+ end
+
+ end
+ end
+
describe 'GET diffs' do
def go(format: 'html')
get :diffs,
@@ -188,7 +222,7 @@ describe Projects::MergeRequestsController do
expect(response).to render_template('diffs')
end
end
-
+
context 'as json' do
it 'renders the diffs template to a string' do
go format: 'json'
@@ -199,6 +233,32 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET diffs with view' do
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid
+ }
+
+ get :diffs, params.merge(extra_params)
+ end
+
+ it 'saves the preferred diff view in a cookie' do
+ go view: 'parallel'
+
+ expect(response.cookies['diff_view']).to eq('parallel')
+ end
+
+ it 'assigns :view param based on cookie' do
+ request.cookies['diff_view'] = 'parallel'
+
+ go
+
+ expect(controller.params[:view]).to eq 'parallel'
+ end
+ end
+
describe 'GET commits' do
def go(format: 'html')
get :commits,
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 18a30033ed8..0ddbec9eac2 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -2,35 +2,41 @@ require "spec_helper"
describe Projects::RepositoriesController do
let(:project) { create(:project) }
- let(:user) { create(:user) }
describe "GET archive" do
- before do
- sign_in(user)
- project.team << [user, :developer]
+ context 'as a guest' do
+ it 'responds with redirect in correct format' do
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
- allow(ArchiveRepositoryService).to receive(:new).and_return(service)
- end
-
- let(:service) { ArchiveRepositoryService.new(project, "master", "zip") }
-
- it "executes ArchiveRepositoryService" do
- expect(ArchiveRepositoryService).to receive(:new).with(project, "master", "zip")
- expect(service).to receive(:execute)
-
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ expect(response.content_type).to start_with 'text/html'
+ expect(response).to be_redirect
+ end
end
- context "when the service raises an error" do
+ context 'as a user' do
+ let(:user) { create(:user) }
before do
- allow(service).to receive(:execute).and_raise("Archive failed")
+ project.team << [user, :developer]
+ sign_in(user)
end
+ it "uses Gitlab::Workhorse" do
+ expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip")
- it "renders Not Found" do
get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ end
+
+ context "when the service raises an error" do
+
+ before do
+ allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
+ end
+
+ it "renders Not Found" do
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
- expect(response.status).to eq(404)
+ expect(response.status).to eq(404)
+ end
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 665526fde93..1893e946f5c 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -9,19 +9,6 @@ describe ProjectsController do
describe "GET show" do
- context "when requested by `go get`" do
- render_views
-
- it "renders the go-import meta tag" do
- get :show, "go-get" => "1", namespace_id: "bogus_namespace", id: "bogus_project"
-
- expect(response.body).to include("name='go-import'")
-
- content = "localhost/bogus_namespace/bogus_project git http://localhost/bogus_namespace/bogus_project.git"
- expect(response.body).to include("content='#{content}'")
- end
- end
-
context "rendering default project view" do
render_views
@@ -86,6 +73,14 @@ describe ProjectsController do
end
end
end
+
+ context "when the url contains .atom" do
+ let(:public_project_with_dot_atom) { build(:project, :public, name: 'my.atom', path: 'my.atom') }
+
+ it 'expect an error creating the project' do
+ expect(public_project_with_dot_atom).not_to be_valid
+ end
+ end
end
describe "#destroy" do
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
new file mode 100644
index 00000000000..9ced397bd4a
--- /dev/null
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+describe SentNotificationsController, type: :controller do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, author: user) }
+ let(:sent_notification) { create(:sent_notification, noteable: issue) }
+
+ describe 'GET #unsubscribe' do
+ it 'returns a 404 when calling without existing id' do
+ get(:unsubscribe, id: '0' * 32)
+
+ expect(response.status).to be 404
+ end
+
+ context 'calling with id' do
+ it 'shows a flash message to the user' do
+ get(:unsubscribe, id: sent_notification.reply_key)
+
+ expect(response.status).to be 302
+
+ expect(response).to redirect_to new_user_session_path
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
+ end
+ end
+end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 104a5f50143..7337ff58be1 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -41,6 +41,7 @@ describe UsersController do
end
describe 'GET #calendar' do
+
it 'renders calendar' do
sign_in(user)
@@ -48,6 +49,23 @@ describe UsersController do
expect(response).to render_template('calendar')
end
+
+ context 'forked project' do
+ let!(:project) { create(:project) }
+ let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ EventCreateService.new.push(project, user, [])
+ EventCreateService.new.push(forked_project, user, [])
+ end
+
+ it 'includes forked projects' do
+ get :calendar, username: user.username
+ expect(assigns(:contributions_calendar).projects.count).to eq(2)
+ end
+ end
end
describe 'GET #calendar_activities' do
diff --git a/spec/factories.rb b/spec/factories.rb
deleted file mode 100644
index d6b4efa9a03..00000000000
--- a/spec/factories.rb
+++ /dev/null
@@ -1,215 +0,0 @@
-include ActionDispatch::TestProcess
-
-FactoryGirl.define do
- sequence :sentence, aliases: [:title, :content] do
- FFaker::Lorem.sentence
- end
-
- sequence :name do
- FFaker::Name.name
- end
-
- sequence :file_name do
- FFaker::Internet.user_name
- end
-
- sequence(:url) { FFaker::Internet.uri('http') }
-
- factory :user, aliases: [:author, :assignee, :owner, :creator] do
- email { FFaker::Internet.email }
- name
- sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" }
- password "12345678"
- confirmed_at { Time.now }
- confirmation_token { nil }
- can_create_group true
-
- trait :admin do
- admin true
- end
-
- trait :two_factor do
- before(:create) do |user|
- user.two_factor_enabled = true
- user.otp_secret = User.generate_otp_secret(32)
- user.generate_otp_backup_codes!
- end
- end
-
- factory :omniauth_user do
- ignore do
- extern_uid '123456'
- provider 'ldapmain'
- end
-
- after(:create) do |user, evaluator|
- user.identities << create(
- :identity,
- provider: evaluator.provider,
- extern_uid: evaluator.extern_uid
- )
- end
- end
-
- factory :admin, traits: [:admin]
- end
-
- factory :group do
- sequence(:name) { |n| "group#{n}" }
- path { name.downcase.gsub(/\s/, '_') }
- type 'Group'
- end
-
- factory :namespace do
- sequence(:name) { |n| "namespace#{n}" }
- path { name.downcase.gsub(/\s/, '_') }
- owner
- end
-
- factory :project_member do
- user
- project
- access_level { ProjectMember::MASTER }
- end
-
- factory :issue do
- title
- author
- project
-
- trait :closed do
- state :closed
- end
-
- trait :reopened do
- state :reopened
- end
-
- factory :closed_issue, traits: [:closed]
- factory :reopened_issue, traits: [:reopened]
- end
-
- factory :event do
- factory :closed_issue_event do
- project
- action { Event::CLOSED }
- target factory: :closed_issue
- author factory: :user
- end
- end
-
- factory :key do
- title
- key do
- "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com"
- end
-
- factory :deploy_key, class: 'DeployKey' do
- end
-
- factory :personal_key do
- user
- end
-
- factory :another_key do
- key do
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ"
- end
-
- factory :another_deploy_key, class: 'DeployKey' do
- end
- end
- end
-
- factory :email do
- user
- email do
- FFaker::Internet.email('alias')
- end
-
- factory :another_email do
- email do
- FFaker::Internet.email('another.alias')
- end
- end
- end
-
- factory :milestone do
- title
- project
-
- trait :closed do
- state :closed
- end
-
- factory :closed_milestone, traits: [:closed]
- end
-
- factory :system_hook do
- url
- end
-
- factory :project_hook do
- url
- end
-
- factory :project_snippet do
- project
- author
- title
- content
- file_name
- end
-
- factory :personal_snippet do
- author
- title
- content
- file_name
-
- 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
-
- factory :snippet do
- author
- title
- content
- file_name
- end
-
- factory :protected_branch do
- name
- project
- end
-
- factory :service do
- type ""
- title "GitLab CI"
- project
- end
-
- factory :service_hook do
- url
- service
- end
-
- factory :deploy_keys_project do
- deploy_key
- project
- end
-
- factory :identity do
- provider 'ldapmain'
- extern_uid 'my-ldap-id'
- end
-end
diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb
index 8d287ded292..d0e8c778518 100644
--- a/spec/factories/abuse_reports.rb
+++ b/spec/factories/abuse_reports.rb
@@ -10,8 +10,6 @@
# updated_at :datetime
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :abuse_report do
reporter factory: :user
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
new file mode 100644
index 00000000000..cf2a2b76bcb
--- /dev/null
+++ b/spec/factories/appearances.rb
@@ -0,0 +1,8 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :appearance do
+ title "MepMep"
+ description "This is my Community Edition instance"
+ end
+end
diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb
index ea0039d39e6..373ca75467e 100644
--- a/spec/factories/broadcast_messages.rb
+++ b/spec/factories/broadcast_messages.rb
@@ -6,22 +6,26 @@
# message :text not null
# starts_at :datetime
# ends_at :datetime
-# alert_type :integer
# created_at :datetime
# updated_at :datetime
# color :string(255)
# font :string(255)
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :broadcast_message do
message "MyText"
- starts_at "2013-11-12 13:43:25"
- ends_at "2013-11-12 13:43:25"
- alert_type 1
- color "#555555"
- font "#BBBBBB"
+ starts_at Date.today
+ ends_at Date.tomorrow
+
+ trait :expired do
+ starts_at 5.days.ago
+ ends_at 3.days.ago
+ end
+
+ trait :future do
+ starts_at 5.days.from_now
+ ends_at 6.days.from_now
+ end
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index f76e826f138..cd49e559b7d 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -1,35 +1,11 @@
-# == Schema Information
-#
-# Table name: builds
-#
-# id :integer not null, primary key
-# project_id :integer
-# status :string(255)
-# finished_at :datetime
-# trace :text
-# created_at :datetime
-# updated_at :datetime
-# started_at :datetime
-# runner_id :integer
-# commit_id :integer
-# coverage :float
-# commands :text
-# job_id :integer
-# name :string(255)
-# deploy :boolean default(FALSE)
-# options :text
-# allow_failure :boolean default(FALSE), not null
-# stage :string(255)
-# trigger_request_id :integer
-#
-
-# Read about factories at https://github.com/thoughtbot/factory_girl
+include ActionDispatch::TestProcess
FactoryGirl.define do
factory :ci_build, class: Ci::Build do
name 'test'
ref 'master'
tag false
+ created_at 'Di 29. Okt 09:50:00 CET 2013'
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
@@ -42,6 +18,30 @@ FactoryGirl.define do
commit factory: :ci_commit
+ trait :success do
+ status 'success'
+ end
+
+ trait :failed do
+ status 'failed'
+ end
+
+ trait :canceled do
+ status 'canceled'
+ end
+
+ trait :running do
+ status 'running'
+ end
+
+ trait :pending do
+ status 'pending'
+ end
+
+ trait :allowed_to_fail do
+ allow_failure true
+ end
+
after(:build) do |build, evaluator|
build.project = build.commit.project
end
@@ -54,5 +54,29 @@ FactoryGirl.define do
factory :ci_build_tag do
tag true
end
+
+ factory :ci_build_with_coverage do
+ coverage 99.9
+ end
+
+ trait :trace do
+ after(:create) do |build, evaluator|
+ build.trace = 'BUILD TRACE'
+ end
+ end
+
+ trait :artifacts do
+ after(:create) do |build, _|
+ build.artifacts_file =
+ fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
+ 'application/zip')
+
+ build.artifacts_metadata =
+ fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
+ 'application/x-gzip')
+
+ build.save!
+ end
+ end
end
end
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb
index b42cafa518a..645cd7ae766 100644
--- a/spec/factories/ci/commits.rb
+++ b/spec/factories/ci/commits.rb
@@ -16,7 +16,6 @@
# gl_project_id :integer
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
FactoryGirl.define do
factory :ci_empty_commit, class: Ci::Commit do
sha '97de212e80737a608d939f648d959671fb0a0142'
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 008d1c5d961..83fccad679f 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -9,8 +9,6 @@
# updated_at :datetime
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :ci_runner_project, class: Ci::RunnerProject do
runner_id 1
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index db759eca9ac..5b645fab32e 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -17,22 +17,18 @@
# architecture :string(255)
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :ci_runner, class: Ci::Runner do
sequence :description do |n|
"My runner#{n}"
end
- platform "darwin"
+ platform "darwin"
+ is_shared false
+ active true
- factory :ci_shared_runner do
+ trait :shared do
is_shared true
end
-
- factory :ci_specific_runner do
- is_shared false
- end
end
end
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
index db053c610cd..6d47d05f8ad 100644
--- a/spec/factories/ci/trigger_requests.rb
+++ b/spec/factories/ci/trigger_requests.rb
@@ -1,8 +1,8 @@
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :ci_trigger_request, class: Ci::TriggerRequest do
factory :ci_trigger_request_with_variables do
+ trigger factory: :ci_trigger
+
variables do
{
TRIGGER_KEY: 'TRIGGER_VALUE'
diff --git a/spec/factories/ci/triggers.rb b/spec/factories/ci/triggers.rb
index fd3afdb1ec2..a27b04424e5 100644
--- a/spec/factories/ci/triggers.rb
+++ b/spec/factories/ci/triggers.rb
@@ -1,5 +1,3 @@
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :ci_trigger_without_token, class: Ci::Trigger do
factory :ci_trigger do
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
new file mode 100644
index 00000000000..856a8e725eb
--- /dev/null
+++ b/spec/factories/ci/variables.rb
@@ -0,0 +1,20 @@
+# == Schema Information
+#
+# Table name: ci_variables
+#
+# id :integer not null, primary key
+# project_id :integer not null
+# key :string(255)
+# value :text
+# encrypted_value :text
+# encrypted_value_salt :string(255)
+# encrypted_value_iv :string(255)
+# gl_project_id :integer
+#
+
+FactoryGirl.define do
+ factory :ci_variable, class: Ci::Variable do
+ sequence(:key) { |n| "VARIABLE_#{n}" }
+ value 'VARIABLE_VALUE'
+ end
+end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 8898b71e2a3..b7c2b32cb13 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -1,11 +1,15 @@
FactoryGirl.define do
factory :commit_status, class: CommitStatus do
- started_at 'Di 29. Okt 09:51:28 CET 2013'
- finished_at 'Di 29. Okt 09:53:28 CET 2013'
name 'default'
status 'success'
description 'commit status'
commit factory: :ci_commit_with_one_job
+ started_at 'Tue, 26 Jan 2016 08:21:42 +0100'
+ finished_at 'Tue, 26 Jan 2016 08:23:42 +0100'
+
+ after(:build) do |build, evaluator|
+ build.project = build.commit.project
+ end
factory :generic_commit_status, class: GenericCommitStatus do
name 'generic'
diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb
new file mode 100644
index 00000000000..27cece487bd
--- /dev/null
+++ b/spec/factories/deploy_keys_projects.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :deploy_keys_project do
+ deploy_key
+ project
+ end
+end
diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb
new file mode 100644
index 00000000000..9794772ac7d
--- /dev/null
+++ b/spec/factories/emails.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :email do
+ user
+ email { FFaker::Internet.email('alias') }
+ end
+end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
new file mode 100644
index 00000000000..90788f30ac9
--- /dev/null
+++ b/spec/factories/events.rb
@@ -0,0 +1,10 @@
+FactoryGirl.define do
+ factory :event do
+ factory :closed_issue_event do
+ project
+ action { Event::CLOSED }
+ target factory: :closed_issue
+ author factory: :user
+ end
+ end
+end
diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb
index 906e4106b32..252bf2747e1 100644
--- a/spec/factories/forked_project_links.rb
+++ b/spec/factories/forked_project_links.rb
@@ -9,8 +9,6 @@
# updated_at :datetime
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :forked_project_link do
association :forked_to_project, factory: :project
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
new file mode 100644
index 00000000000..4a3a155d7ff
--- /dev/null
+++ b/spec/factories/groups.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :group do
+ sequence(:name) { |n| "group#{n}" }
+ path { name.downcase.gsub(/\s/, '_') }
+ type 'Group'
+ end
+end
diff --git a/spec/factories/identities.rb b/spec/factories/identities.rb
new file mode 100644
index 00000000000..26ef6f18698
--- /dev/null
+++ b/spec/factories/identities.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :identity do
+ provider 'ldapmain'
+ extern_uid 'my-ldap-id'
+ end
+end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
new file mode 100644
index 00000000000..e72aa9479b7
--- /dev/null
+++ b/spec/factories/issues.rb
@@ -0,0 +1,22 @@
+FactoryGirl.define do
+ factory :issue do
+ title
+ author
+ project
+
+ trait :confidential do
+ confidential true
+ end
+
+ trait :closed do
+ state :closed
+ end
+
+ trait :reopened do
+ state :reopened
+ end
+
+ factory :closed_issue, traits: [:closed]
+ factory :reopened_issue, traits: [:reopened]
+ end
+end
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
new file mode 100644
index 00000000000..d69c5b38d0a
--- /dev/null
+++ b/spec/factories/keys.rb
@@ -0,0 +1,24 @@
+FactoryGirl.define do
+ factory :key do
+ title
+ key do
+ "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com"
+ end
+
+ factory :deploy_key, class: 'DeployKey' do
+ end
+
+ factory :personal_key do
+ user
+ end
+
+ factory :another_key do
+ key do
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ"
+ end
+
+ factory :another_deploy_key, class: 'DeployKey' do
+ end
+ end
+ end
+end
diff --git a/spec/factories/label_links.rb b/spec/factories/label_links.rb
index bd304b5db6b..2939d4307c5 100644
--- a/spec/factories/label_links.rb
+++ b/spec/factories/label_links.rb
@@ -10,8 +10,6 @@
# updated_at :datetime
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :label_link do
label
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index 8b12ee11af5..ea2be8928d5 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -11,11 +11,9 @@
# template :boolean default(FALSE)
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :label do
- title "Bug"
+ sequence(:title) { |n| "label#{n}" }
color "#990000"
project
end
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index 2da107ba24b..327858ce435 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -10,7 +10,7 @@
# file :string(255)
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
+include ActionDispatch::TestProcess
FactoryGirl.define do
factory :lfs_object do
diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb
index 3772236a77a..50b45843c99 100644
--- a/spec/factories/lfs_objects_projects.rb
+++ b/spec/factories/lfs_objects_projects.rb
@@ -9,8 +9,6 @@
# updated_at :datetime
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :lfs_objects_project do
lfs_object
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 5b4d7f41bc4..e281e2f227b 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -2,25 +2,29 @@
#
# Table name: merge_requests
#
-# id :integer not null, primary key
-# target_branch :string(255) not null
-# source_branch :string(255) not null
-# source_project_id :integer not null
-# author_id :integer
-# assignee_id :integer
-# title :string(255)
-# created_at :datetime
-# updated_at :datetime
-# milestone_id :integer
-# state :string(255)
-# merge_status :string(255)
-# target_project_id :integer not null
-# iid :integer
-# description :text
-# position :integer default(0)
-# locked_at :datetime
-# updated_by_id :integer
-# merge_error :string(255)
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# source_project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# created_at :datetime
+# updated_at :datetime
+# milestone_id :integer
+# state :string(255)
+# merge_status :string(255)
+# target_project_id :integer not null
+# iid :integer
+# description :text
+# position :integer default(0)
+# locked_at :datetime
+# updated_by_id :integer
+# merge_error :string(255)
+# merge_params :text
+# merge_when_build_succeeds :boolean default(FALSE), not null
+# merge_user_id :integer
+# merge_commit_sha :string
#
FactoryGirl.define do
@@ -47,11 +51,20 @@ 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"
end
+ trait :merged do
+ state :merged
+ end
+
trait :closed do
state :closed
end
@@ -65,11 +78,22 @@ FactoryGirl.define do
target_branch "master"
end
+ trait :rebased do
+ source_branch "markdown"
+ target_branch "improve/awesome"
+ end
+
+ trait :diverged do
+ source_branch "feature"
+ target_branch "master"
+ end
+
trait :merge_when_build_succeeds do
merge_when_build_succeeds true
merge_user author
end
+ factory :merged_merge_request, traits: [:merged]
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs]
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
new file mode 100644
index 00000000000..e9e85962fe4
--- /dev/null
+++ b/spec/factories/milestones.rb
@@ -0,0 +1,12 @@
+FactoryGirl.define do
+ factory :milestone do
+ title
+ project
+
+ trait :closed do
+ state :closed
+ end
+
+ factory :closed_milestone, traits: [:closed]
+ end
+end
diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb
new file mode 100644
index 00000000000..1b1fc4ce80d
--- /dev/null
+++ b/spec/factories/namespaces.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :namespace do
+ sequence(:name) { |n| "namespace#{n}" }
+ path { name.downcase.gsub(/\s/, '_') }
+ owner
+ end
+end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 35a20adeef3..e5dcb159014 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -21,6 +21,8 @@
require_relative '../support/repo_helpers'
+include ActionDispatch::TestProcess
+
FactoryGirl.define do
factory :note do
project
@@ -34,6 +36,8 @@ FactoryGirl.define do
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff]
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :system_note, traits: [:system]
+ factory :downvote_note, traits: [:award, :downvote]
+ factory :upvote_note, traits: [:award, :upvote]
trait :on_commit do
project
@@ -65,6 +69,18 @@ FactoryGirl.define do
system true
end
+ trait :award do
+ is_award true
+ end
+
+ trait :downvote do
+ note "thumbsdown"
+ end
+
+ trait :upvote do
+ note "thumbsup"
+ end
+
trait :with_attachment do
attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
end
diff --git a/spec/factories/personal_snippets.rb b/spec/factories/personal_snippets.rb
new file mode 100644
index 00000000000..0f13b2c1020
--- /dev/null
+++ b/spec/factories/personal_snippets.rb
@@ -0,0 +1,4 @@
+FactoryGirl.define do
+ factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do
+ end
+end
diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb
new file mode 100644
index 00000000000..e73cc05f9d7
--- /dev/null
+++ b/spec/factories/project_group_links.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :project_group_link do
+ project
+ group
+ end
+end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
new file mode 100644
index 00000000000..94dd935a039
--- /dev/null
+++ b/spec/factories/project_hooks.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :project_hook do
+ url { FFaker::Internet.uri('http') }
+ end
+end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
new file mode 100644
index 00000000000..cf3659ba275
--- /dev/null
+++ b/spec/factories/project_members.rb
@@ -0,0 +1,27 @@
+FactoryGirl.define do
+ factory :project_member do
+ user
+ project
+ master
+
+ trait :guest do
+ access_level ProjectMember::GUEST
+ end
+
+ trait :reporter do
+ access_level ProjectMember::REPORTER
+ end
+
+ trait :developer do
+ access_level ProjectMember::DEVELOPER
+ end
+
+ trait :master do
+ access_level ProjectMember::MASTER
+ end
+
+ trait :owner do
+ access_level ProjectMember::OWNER
+ end
+ end
+end
diff --git a/spec/factories/project_snippets.rb b/spec/factories/project_snippets.rb
new file mode 100644
index 00000000000..d681a2c8483
--- /dev/null
+++ b/spec/factories/project_snippets.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
+ project
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 112213377ff..c14b99606ba 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -29,6 +29,13 @@
# import_source :string(255)
# commit_count :integer default(0)
# import_error :text
+# ci_id :integer
+# builds_enabled :boolean default(TRUE), not null
+# shared_runners_enabled :boolean default(TRUE), not null
+# runners_token :string
+# build_coverage_regex :string
+# build_allow_git_fetch :boolean default(TRUE), not null
+# build_timeout :integer default(3600), not null
#
FactoryGirl.define do
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
new file mode 100644
index 00000000000..28ed8078157
--- /dev/null
+++ b/spec/factories/protected_branches.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :protected_branch do
+ name
+ project
+ end
+end
diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb
index 43d09b17534..7f331c37256 100644
--- a/spec/factories/releases.rb
+++ b/spec/factories/releases.rb
@@ -10,8 +10,6 @@
# updated_at :datetime
#
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
FactoryGirl.define do
factory :release do
tag "v1.1.0"
diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb
new file mode 100644
index 00000000000..78eb929c6e7
--- /dev/null
+++ b/spec/factories/sent_notifications.rb
@@ -0,0 +1,8 @@
+FactoryGirl.define do
+ factory :sent_notification do
+ project
+ recipient factory: :user
+ noteable factory: :issue
+ reply_key "0123456789abcdef" * 2
+ end
+end
diff --git a/spec/factories/service_hooks.rb b/spec/factories/service_hooks.rb
new file mode 100644
index 00000000000..6dd6af63f3e
--- /dev/null
+++ b/spec/factories/service_hooks.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :service_hook do
+ url { FFaker::Internet.uri('http') }
+ service
+ end
+end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
new file mode 100644
index 00000000000..9de78d68280
--- /dev/null
+++ b/spec/factories/services.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :service do
+ project
+ end
+end
diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb
new file mode 100644
index 00000000000..365f12a0c95
--- /dev/null
+++ b/spec/factories/snippets.rb
@@ -0,0 +1,28 @@
+FactoryGirl.define do
+ sequence :title, aliases: [:content] do
+ FFaker::Lorem.sentence
+ end
+
+ sequence :file_name do
+ FFaker::Internet.user_name
+ end
+
+ factory :snippet do
+ author
+ title
+ content
+ file_name
+
+ trait :public do
+ visibility_level Snippet::PUBLIC
+ end
+
+ trait :internal do
+ visibility_level Snippet::INTERNAL
+ end
+
+ trait :private do
+ visibility_level Snippet::PRIVATE
+ end
+ end
+end
diff --git a/spec/factories/spam_logs.rb b/spec/factories/spam_logs.rb
new file mode 100644
index 00000000000..a4f6d291269
--- /dev/null
+++ b/spec/factories/spam_logs.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :spam_log do
+ user
+ source_ip { FFaker::Internet.ip_v4_address }
+ noteable_type 'Issue'
+ title { FFaker::Lorem.sentence }
+ description { FFaker::Lorem.paragraph(5) }
+ end
+end
diff --git a/spec/factories/system_hooks.rb b/spec/factories/system_hooks.rb
new file mode 100644
index 00000000000..c786e9cb79b
--- /dev/null
+++ b/spec/factories/system_hooks.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :system_hook do
+ url { FFaker::Internet.uri('http') }
+ end
+end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
new file mode 100644
index 00000000000..bd85b1d798a
--- /dev/null
+++ b/spec/factories/todos.rb
@@ -0,0 +1,34 @@
+# == Schema Information
+#
+# Table name: todos
+#
+# id :integer not null, primary key
+# user_id :integer not null
+# project_id :integer not null
+# target_id :integer not null
+# 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
+#
+
+FactoryGirl.define do
+ factory :todo do
+ project
+ author
+ user
+ target factory: :issue
+ action { Todo::ASSIGNED }
+
+ trait :assigned do
+ action { Todo::ASSIGNED }
+ end
+
+ trait :mentioned do
+ action { Todo::MENTIONED }
+ end
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
new file mode 100644
index 00000000000..a5c60c51c5b
--- /dev/null
+++ b/spec/factories/users.rb
@@ -0,0 +1,43 @@
+FactoryGirl.define do
+ sequence(:name) { FFaker::Name.name }
+
+ factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator] do
+ email { FFaker::Internet.email }
+ name
+ sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" }
+ password "12345678"
+ confirmed_at { Time.now }
+ confirmation_token { nil }
+ can_create_group true
+
+ trait :admin do
+ admin true
+ end
+
+ trait :two_factor do
+ before(:create) do |user|
+ user.two_factor_enabled = true
+ user.otp_secret = User.generate_otp_secret(32)
+ user.otp_grace_period_started_at = Time.now
+ user.generate_otp_backup_codes!
+ end
+ end
+
+ factory :omniauth_user do
+ transient do
+ extern_uid '123456'
+ provider 'ldapmain'
+ end
+
+ after(:create) do |user, evaluator|
+ user.identities << create(
+ :identity,
+ provider: evaluator.provider,
+ extern_uid: evaluator.extern_uid
+ )
+ end
+ end
+
+ factory :admin, traits: [:admin]
+ end
+end
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index 72764b1629d..2e9851fb442 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -1,69 +1,98 @@
require 'spec_helper'
-describe "Admin Builds" do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:build) { FactoryGirl.create :ci_build, commit: commit }
-
+describe 'Admin Builds' do
before do
login_as :admin
end
- describe "GET /admin/builds" do
- before do
- build
- visit admin_builds_path
- end
-
- it { expect(page).to have_content "Running" }
- it { expect(page).to have_content build.short_sha }
- end
+ describe 'GET /admin/builds' do
+ let(:commit) { create(:ci_commit) }
- describe "Tabs" do
- it "shows all builds" do
- FactoryGirl.create :ci_build, commit: commit, status: "pending"
- FactoryGirl.create :ci_build, commit: commit, status: "running"
- FactoryGirl.create :ci_build, commit: commit, status: "success"
- FactoryGirl.create :ci_build, commit: commit, status: "failed"
+ context 'All tab' do
+ context 'when have builds' do
+ it 'shows all builds' do
+ create(:ci_build, commit: commit, status: :pending)
+ create(:ci_build, commit: commit, status: :running)
+ create(:ci_build, commit: commit, status: :success)
+ create(:ci_build, commit: commit, status: :failed)
- visit admin_builds_path
+ visit admin_builds_path
- within ".center-top-menu" do
- click_on "All"
+ expect(page).to have_selector('.nav-links li.active', text: 'All')
+ expect(page.all('.build-link').size).to eq(4)
+ expect(page).to have_link 'Cancel all'
+ end
end
- expect(page.all(".build-link").size).to eq(4)
+ context 'when have no builds' do
+ it 'shows a message' do
+ visit admin_builds_path
+
+ expect(page).to have_selector('.nav-links li.active', text: 'All')
+ expect(page).to have_content 'No builds to show'
+ expect(page).not_to have_link 'Cancel all'
+ end
+ end
end
- it "shows finished builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
+ context 'Running tab' do
+ context 'when have running builds' do
+ it 'shows running builds' do
+ build1 = create(:ci_build, commit: commit, status: :pending)
+ build2 = create(:ci_build, commit: commit, status: :success)
+ build3 = create(:ci_build, commit: commit, status: :failed)
+
+ visit admin_builds_path(scope: :running)
+
+ expect(page).to have_selector('.nav-links li.active', text: 'Running')
+ expect(page.find('.build-link')).to have_content(build1.id)
+ expect(page.find('.build-link')).not_to have_content(build2.id)
+ expect(page.find('.build-link')).not_to have_content(build3.id)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
- visit admin_builds_path
+ context 'when have no builds running' do
+ it 'shows a message' do
+ create(:ci_build, commit: commit, status: :success)
- within ".center-top-menu" do
- click_on "Finished"
- end
+ visit admin_builds_path(scope: :running)
- expect(page.find(".build-link")).not_to have_content(build.id)
- expect(page.find(".build-link")).not_to have_content(build1.id)
- expect(page.find(".build-link")).to have_content(build2.id)
+ expect(page).to have_selector('.nav-links li.active', text: 'Running')
+ expect(page).to have_content 'No builds to show'
+ expect(page).not_to have_link 'Cancel all'
+ end
+ end
end
- it "shows running builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
- build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+ context 'Finished tab' do
+ context 'when have finished builds' do
+ it 'shows finished builds' do
+ build1 = create(:ci_build, commit: commit, status: :pending)
+ build2 = create(:ci_build, commit: commit, status: :running)
+ build3 = create(:ci_build, commit: commit, status: :success)
+
+ visit admin_builds_path(scope: :finished)
+
+ expect(page).to have_selector('.nav-links li.active', text: 'Finished')
+ expect(page.find('.build-link')).not_to have_content(build1.id)
+ expect(page.find('.build-link')).not_to have_content(build2.id)
+ expect(page.find('.build-link')).to have_content(build3.id)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
- visit admin_builds_path
+ context 'when have no builds finished' do
+ it 'shows a message' do
+ create(:ci_build, commit: commit, status: :running)
- within ".center-top-menu" do
- click_on "Running"
- end
+ visit admin_builds_path(scope: :finished)
- expect(page.find(".build-link")).to have_content(build.id)
- expect(page.find(".build-link")).not_to have_content(build2.id)
- expect(page.find(".build-link")).not_to have_content(build3.id)
+ expect(page).to have_selector('.nav-links li.active', text: 'Finished')
+ expect(page).to have_content 'No builds to show'
+ expect(page).to have_link 'Cancel all'
+ end
+ end
end
end
end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index f0031a0a247..6da3a857b3f 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -8,18 +8,18 @@ describe "Builds" do
@commit = FactoryGirl.create :ci_commit
@build = FactoryGirl.create :ci_build, commit: @commit
@project = @commit.project
- @project.team << [@user, :master]
+ @project.team << [@user, :developer]
end
describe "GET /:project/builds" do
context "Running scope" do
before do
@build.run!
- visit namespace_project_builds_path(@project.namespace, @project)
+ visit namespace_project_builds_path(@project.namespace, @project, scope: :running)
end
- it { expect(page).to have_content 'Running' }
- it { expect(page).to have_content 'Cancel running' }
+ it { expect(page).to have_selector('.nav-links li.active', text: 'Running') }
+ it { expect(page).to have_link 'Cancel running' }
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
@@ -31,21 +31,22 @@ describe "Builds" do
visit namespace_project_builds_path(@project.namespace, @project, scope: :finished)
end
+ it { expect(page).to have_selector('.nav-links li.active', text: 'Finished') }
it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to have_content 'Cancel running' }
+ it { expect(page).to have_link 'Cancel running' }
end
context "All builds" do
before do
@project.builds.running_or_pending.each(&:success)
- visit namespace_project_builds_path(@project.namespace, @project, scope: :all)
+ visit namespace_project_builds_path(@project.namespace, @project)
end
- it { expect(page).to have_content 'All' }
+ it { expect(page).to have_selector('.nav-links li.active', text: 'All') }
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
- it { expect(page).to_not have_content 'Cancel running' }
+ it { expect(page).to_not have_link 'Cancel running' }
end
end
@@ -56,8 +57,12 @@ describe "Builds" do
click_link "Cancel running"
end
- it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to_not have_content 'Cancel running' }
+ it { expect(page).to have_selector('.nav-links li.active', text: 'All') }
+ it { expect(page).to have_content 'canceled' }
+ it { expect(page).to have_content @build.short_sha }
+ it { expect(page).to have_content @build.ref }
+ it { expect(page).to have_content @build.name }
+ it { expect(page).to_not have_link 'Cancel running' }
end
describe "GET /:project/builds/:id" do
@@ -75,7 +80,11 @@ describe "Builds" do
visit namespace_project_build_path(@project.namespace, @project, @build)
end
- it { expect(page).to have_content 'Download artifacts' }
+ it 'has button to download artifacts' do
+ page.within('.artifacts') do
+ expect(page).to have_content 'Download'
+ end
+ end
end
end
@@ -106,7 +115,7 @@ describe "Builds" do
before do
@build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build)
- click_link 'Download artifacts'
+ page.within('.artifacts') { click_link 'Download' }
end
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index e6e73e5e67c..30e29d9d552 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -35,5 +35,13 @@ describe 'CI Lint' do
expect(page).to have_content('Error: Please provide content of .gitlab-ci.yml')
end
end
+
+ describe 'YAML revalidate' do
+ let(:yaml_content) { 'my yaml content' }
+
+ it 'loads previous YAML content after validation' do
+ expect(page).to have_field('content', with: 'my yaml content', type: 'textarea')
+ end
+ end
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index fe7f07f5b75..dacaa96d760 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -8,7 +8,6 @@ describe 'Commits' do
describe 'CI' do
before do
login_as :user
- project.team << [@user, :master]
stub_ci_commit_to_return_yaml_file
end
@@ -16,83 +15,149 @@ describe 'Commits' do
FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha
end
- let!(:build) { FactoryGirl.create :ci_build, commit: commit }
+ context 'commit status is Generic Commit Status' do
+ let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit }
- describe 'Project commits' do
before do
- visit namespace_project_commits_path(project.namespace, project, :master)
+ project.team << [@user, :reporter]
end
- it 'should show build status' do
- page.within("//li[@id='commit-#{commit.short_sha}']") do
- expect(page).to have_css(".ci-status-link")
+ describe 'Commit builds' do
+ before do
+ visit ci_status_path(commit)
end
- end
- end
- describe 'Commit builds' do
- before do
- visit ci_status_path(commit)
- end
+ it { expect(page).to have_content commit.sha[0..7] }
- it { expect(page).to have_content commit.sha[0..7] }
- it { expect(page).to have_content commit.git_commit_message }
- it { expect(page).to have_content commit.git_author_name }
+ it 'contains generic commit status build' do
+ page.within('.table-holder') do
+ expect(page).to have_content "##{status.id}" # build id
+ expect(page).to have_content 'generic' # build name
+ end
+ end
+ end
end
- context 'Download artifacts' do
+ context 'commit status is Ci Build' do
+ let!(:build) { FactoryGirl.create :ci_build, commit: commit }
let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
- before do
- build.update_attributes(artifacts_file: artifacts_file)
- end
+ context 'when logged as developer' do
+ before do
+ project.team << [@user, :developer]
+ end
- it do
- visit ci_status_path(commit)
- click_on 'Download artifacts'
- expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type)
- end
- end
+ describe 'Project commits' do
+ before do
+ visit namespace_project_commits_path(project.namespace, project, :master)
+ end
- describe 'Cancel all builds' do
- it 'cancels commit' do
- visit ci_status_path(commit)
- click_on 'Cancel running'
- expect(page).to have_content 'canceled'
- end
- end
+ it 'should show build status' do
+ page.within("//li[@id='commit-#{commit.short_sha}']") do
+ expect(page).to have_css(".ci-status-link")
+ end
+ end
+ end
+
+ describe 'Commit builds' do
+ before do
+ visit ci_status_path(commit)
+ end
+
+ it { expect(page).to have_content commit.sha[0..7] }
+ it { expect(page).to have_content commit.git_commit_message }
+ it { expect(page).to have_content commit.git_author_name }
+ end
- describe 'Cancel build' do
- it 'cancels build' do
- visit ci_status_path(commit)
- click_on 'Cancel'
- expect(page).to have_content 'canceled'
+ context 'Download artifacts' do
+ before do
+ build.update_attributes(artifacts_file: artifacts_file)
+ end
+
+ it do
+ visit ci_status_path(commit)
+ click_on 'Download artifacts'
+ expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type)
+ end
+ end
+
+ describe 'Cancel all builds' do
+ it 'cancels commit' do
+ visit ci_status_path(commit)
+ click_on 'Cancel running'
+ expect(page).to have_content 'canceled'
+ end
+ end
+
+ describe 'Cancel build' do
+ it 'cancels build' do
+ visit ci_status_path(commit)
+ click_on 'Cancel'
+ expect(page).to have_content 'canceled'
+ end
+ end
+
+ describe '.gitlab-ci.yml not found warning' do
+ context 'ci builds enabled' do
+ it "does not show warning" do
+ visit ci_status_path(commit)
+ expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ end
+
+ it 'shows warning' do
+ stub_ci_commit_yaml_file(nil)
+ visit ci_status_path(commit)
+ expect(page).to have_content '.gitlab-ci.yml not found in this commit'
+ end
+ end
+
+ context 'ci builds disabled' do
+ before do
+ stub_ci_builds_disabled
+ stub_ci_commit_yaml_file(nil)
+ visit ci_status_path(commit)
+ end
+
+ it 'does not show warning' do
+ expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ end
+ end
+ end
end
- end
- describe '.gitlab-ci.yml not found warning' do
- context 'ci builds enabled' do
- it "does not show warning" do
+ context "when logged as reporter" do
+ before do
+ project.team << [@user, :reporter]
+ build.update_attributes(artifacts_file: artifacts_file)
visit ci_status_path(commit)
- expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
end
- it 'shows warning' do
- stub_ci_commit_yaml_file(nil)
- visit ci_status_path(commit)
- expect(page).to have_content '.gitlab-ci.yml not found in this commit'
+ it do
+ expect(page).to have_content commit.sha[0..7]
+ expect(page).to have_content commit.git_commit_message
+ expect(page).to have_content commit.git_author_name
+ expect(page).to have_link('Download artifacts')
+ expect(page).to_not have_link('Cancel running')
+ expect(page).to_not have_link('Retry failed')
end
end
- context 'ci builds disabled' do
+ context 'when accessing internal project with disallowed access' do
before do
- stub_ci_builds_disabled
- stub_ci_commit_yaml_file(nil)
+ project.update(
+ visibility_level: Gitlab::VisibilityLevel::INTERNAL,
+ public_builds: false)
+ build.update_attributes(artifacts_file: artifacts_file)
visit ci_status_path(commit)
end
- it 'does not show warning' do
- expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ it do
+ expect(page).to have_content commit.sha[0..7]
+ expect(page).to have_content commit.git_commit_message
+ expect(page).to have_content commit.git_author_name
+ expect(page).to_not have_link('Download artifacts')
+ expect(page).to_not have_link('Cancel running')
+ expect(page).to_not have_link('Retry failed')
end
end
end
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 38c8d343ce3..f6e33f651c4 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Issue filtering by Milestone', feature: true do
- include Select2Helper
-
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -13,7 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::None.title)
- expect(page).to have_css('.title', count: 1)
+ expect(page).to have_css('.issue .title', count: 1)
end
scenario 'filters by a specific Milestone', js: true do
@@ -23,7 +21,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(milestone.title)
- expect(page).to have_css('.title', count: 1)
+ expect(page).to have_css('.issue .title', count: 1)
end
def visit_issues(project)
@@ -31,6 +29,9 @@ feature 'Issue filtering by Milestone', feature: true do
end
def filter_by_milestone(title)
- select2(title, from: '#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/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
new file mode 100644
index 00000000000..9219b767547
--- /dev/null
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'Start new branch from an issue', feature: true do
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ context "for team members" do
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ it 'shown the new branch button', js: false do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).to have_link "New Branch"
+ end
+
+ context "when there is a referenced merge request" do
+ let(:note) do
+ create(:note, :on_issue, :system, project: project,
+ note: "mentioned in !#{referenced_mr.iid}")
+ end
+ let(:referenced_mr) do
+ create(:merge_request, :simple, source_project: project, target_project: project,
+ description: "Fixes ##{issue.iid}", author: user)
+ end
+
+ before do
+ issue.notes << note
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it "hides the new branch button", js: true do
+ expect(page).not_to have_link "New Branch"
+ expect(page).to have_content /1 Related Merge Request/
+ end
+ end
+ end
+
+ context "for visiters" do
+ it 'no button is shown', js: false do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ expect(page).not_to have_link "New Branch"
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index a2fb3e4c75d..e844e681ebf 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -127,15 +127,15 @@ describe 'Issues', feature: true do
it 'sorts by newest' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created)
- expect(first_issue).to include('foo')
- expect(last_issue).to include('baz')
+ expect(first_issue).to include('baz')
+ expect(last_issue).to include('foo')
end
it 'sorts by oldest' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created)
- expect(first_issue).to include('baz')
- expect(last_issue).to include('foo')
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('baz')
end
it 'sorts by most recently updated' do
@@ -190,8 +190,8 @@ describe 'Issues', feature: true do
sort: sort_value_oldest_created,
assignee_id: user2.id)
- expect(first_issue).to include('bar')
- expect(last_issue).to include('foo')
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('bar')
expect(page).not_to have_content 'baz'
end
end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 2451e56fe7c..4433ef2d6f1 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -1,6 +1,32 @@
require 'spec_helper'
feature 'Login', feature: true do
+ describe 'initial login after setup' do
+ it 'allows the initial admin to create a password' do
+ # This behavior is dependent on there only being one user
+ User.delete_all
+
+ user = create(:admin, password_automatically_set: true)
+
+ visit root_path
+ expect(current_path).to eq edit_user_password_path
+ expect(page).to have_content('Please create a password for your new account.')
+
+ fill_in 'user_password', with: 'password'
+ fill_in 'user_password_confirmation', with: 'password'
+ click_button 'Change your password'
+
+ expect(current_path).to eq new_user_session_path
+ expect(page).to have_content(I18n.t('devise.passwords.updated_not_active'))
+
+ fill_in 'user_login', with: user.username
+ fill_in 'user_password', with: 'password'
+ click_button 'Sign in'
+
+ expect(current_path).to eq root_path
+ end
+ end
+
describe 'with two-factor authentication' do
context 'with valid username/password' do
let(:user) { create(:user, :two_factor) }
@@ -112,10 +138,10 @@ feature 'Login', feature: true do
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
- expect(page).to have_content('You must configure Two-Factor Authentication in your account until')
+ expect(page).to have_content('You must enable Two-factor Authentication for your account before')
end
- it 'two-factor configuration is skippable' do
+ it 'disallows skipping two-factor configuration' do
expect(current_path).to eq new_profile_two_factor_auth_path
click_link 'Configure it later'
@@ -128,10 +154,10 @@ feature 'Login', feature: true do
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
- expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
+ expect(page).to have_content('You must enable Two-factor Authentication for your account.')
end
- it 'two-factor configuration is not skippable' do
+ it 'disallows skipping two-factor configuration' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
@@ -146,7 +172,7 @@ feature 'Login', feature: true do
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
- expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
+ expect(page).to have_content('You must enable Two-factor Authentication for your account.')
end
end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index fdd8cf07b12..12fd8d37210 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -175,13 +175,15 @@ describe 'GitLab Markdown', feature: true do
end
end
- context 'default pipeline' do
- before(:all) do
- @feat = MarkdownFeature.new
+ before(:all) do
+ @feat = MarkdownFeature.new
- # `markdown` helper expects a `@project` variable
- @project = @feat.project
+ # `markdown` helper expects a `@project` variable
+ @project = @feat.project
+ end
+ context 'default pipeline' do
+ before(:all) do
@html = markdown(@feat.raw_markdown)
end
@@ -212,6 +214,7 @@ describe 'GitLab Markdown', feature: true do
expect(doc).to reference_commit_ranges
expect(doc).to reference_commits
expect(doc).to reference_labels
+ expect(doc).to reference_milestones
end
end
@@ -220,6 +223,57 @@ describe 'GitLab Markdown', feature: true do
end
end
+ context 'wiki pipeline' do
+ before do
+ @project_wiki = @feat.project_wiki
+
+ file = Gollum::File.new(@project_wiki.wiki)
+ expect(file).to receive(:path).and_return('images/example.jpg')
+ expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
+
+ @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
+ end
+
+ it_behaves_like 'all pipelines'
+
+ it 'includes RelativeLinkFilter' do
+ expect(doc).not_to parse_relative_links
+ end
+
+ it 'includes EmojiFilter' do
+ expect(doc).to parse_emoji
+ end
+
+ it 'includes TableOfContentsFilter' do
+ expect(doc).to create_header_links
+ end
+
+ it 'includes AutolinkFilter' do
+ expect(doc).to create_autolinks
+ end
+
+ it 'includes all reference filters' do
+ aggregate_failures do
+ expect(doc).to reference_users
+ expect(doc).to reference_issues
+ expect(doc).to reference_merge_requests
+ expect(doc).to reference_snippets
+ expect(doc).to reference_commit_ranges
+ expect(doc).to reference_commits
+ expect(doc).to reference_labels
+ expect(doc).to reference_milestones
+ end
+ end
+
+ it 'includes TaskListFilter' do
+ expect(doc).to parse_task_lists
+ end
+
+ it 'includes GollumTagsFilter' do
+ expect(doc).to parse_gollum_tags
+ end
+ end
+
# Fake a `current_user` helper
def current_user
@feat.user
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index f70214e1122..b76e4c74c79 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
- include Select2Helper
-
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -31,6 +29,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
end
def filter_by_milestone(title)
- select2(title, from: '#milestone_title')
+ find(".js-milestone-select").click
+ find(".milestone-filter a", text: title).click
end
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index f0fc6916c4d..d9a8058efd9 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -22,7 +22,7 @@ describe 'Comments', feature: true do
it 'should be valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
expect(find('.js-main-target-form input[type=submit]').value).
- to eq('Add Comment')
+ to eq('Comment')
page.within('.js-main-target-form') do
expect(page).not_to have_link('Cancel')
end
@@ -49,7 +49,7 @@ describe 'Comments', feature: true do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: 'This is awsome!'
find('.js-md-preview-button').click
- click_button 'Add Comment'
+ click_button 'Comment'
end
end
@@ -167,7 +167,7 @@ describe 'Comments', feature: true do
end
it 'should be removed when canceled' do
- page.within(".diff-file form[rel$='#{line_code}']") do
+ page.within(".diff-file form[id$='#{line_code}']") do
find('.js-close-discussion-note-form').trigger('click')
end
@@ -202,7 +202,7 @@ describe 'Comments', feature: true do
before do
page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do
fill_in 'note[note]', with: 'Another comment on line 10'
- click_button('Add Comment')
+ click_button('Comment')
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 74b148f5d17..ed97b6cb577 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -80,8 +80,29 @@ feature 'Project', feature: true do
visit namespace_project_path(project.namespace, project)
end
- it { expect(page).to have_content('You have Master access to this project.') }
- it { expect(page).to have_link('Leave this project') }
+ it 'click project-settings and find leave project' do
+ find('#project-settings-button').click
+ expect(page).to have_link('Leave Project')
+ end
+ end
+
+ describe 'project title' do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ before do
+ login_with(user)
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'click toggle and show dropdown', js: true do
+ find('.js-projects-dropdown-toggle').click
+ wait_for_ajax
+ expect(page).to have_css('.select2-results li', count: 1)
+ end
end
def remove_with_confirm(button_text, confirm_with)
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index d97831aae14..e8886e7edf9 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -17,10 +17,10 @@ describe "Runners" do
@project3 = FactoryGirl.create :empty_project
@project3.team << [user, :developer]
- @shared_runner = FactoryGirl.create :ci_shared_runner
- @specific_runner = FactoryGirl.create :ci_specific_runner
- @specific_runner2 = FactoryGirl.create :ci_specific_runner
- @specific_runner3 = FactoryGirl.create :ci_specific_runner
+ @shared_runner = FactoryGirl.create :ci_runner, :shared
+ @specific_runner = FactoryGirl.create :ci_runner
+ @specific_runner2 = FactoryGirl.create :ci_runner
+ @specific_runner3 = FactoryGirl.create :ci_runner
@project.runners << @specific_runner
@project2.runners << @specific_runner2
@project3.runners << @specific_runner3
@@ -84,7 +84,7 @@ describe "Runners" do
before do
@project = FactoryGirl.create :empty_project
@project.team << [user, :master]
- @specific_runner = FactoryGirl.create :ci_specific_runner
+ @specific_runner = FactoryGirl.create :ci_runner
@project.runners << @specific_runner
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 57563add74c..f88c591d897 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -8,10 +8,12 @@ describe "Internal Project Access", feature: true do
let(:master) { create(:user) }
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
+ let(:external_team_member) { create(:user, external: true) }
before do
# full access
project.team << [master, :master]
+ project.team << [external_team_member, :master]
# readonly
project.team << [reporter, :reporter]
@@ -34,6 +36,8 @@ describe "Internal Project Access", feature: true do
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
@@ -45,6 +49,8 @@ describe "Internal Project Access", feature: true do
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
@@ -56,6 +62,8 @@ describe "Internal Project Access", feature: true do
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
@@ -67,6 +75,8 @@ describe "Internal Project Access", feature: true do
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
@@ -78,6 +88,8 @@ describe "Internal Project Access", feature: true do
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
@@ -89,22 +101,23 @@ describe "Internal Project Access", feature: true do
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/blob" do
- before do
- commit = project.repository.commit
- path = '.gitignore'
- @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path))
- end
+ let(:commit) { project.repository.commit }
+ subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
- it { expect(@blob_path).to be_allowed_for master }
- it { expect(@blob_path).to be_allowed_for reporter }
- it { expect(@blob_path).to be_allowed_for :admin }
- it { expect(@blob_path).to be_allowed_for guest }
- it { expect(@blob_path).to be_allowed_for :user }
- it { expect(@blob_path).to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for master }
+ 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
@@ -115,6 +128,8 @@ describe "Internal Project Access", feature: true do
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
@@ -126,6 +141,8 @@ describe "Internal Project Access", feature: true do
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
@@ -137,6 +154,8 @@ describe "Internal Project Access", feature: true do
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
@@ -149,6 +168,8 @@ describe "Internal Project Access", feature: true do
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
@@ -160,6 +181,8 @@ describe "Internal Project Access", feature: true do
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
@@ -171,6 +194,8 @@ describe "Internal Project Access", feature: true do
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
@@ -182,6 +207,8 @@ describe "Internal Project Access", feature: true do
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
@@ -193,6 +220,8 @@ describe "Internal Project Access", feature: true do
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
@@ -209,6 +238,8 @@ describe "Internal Project Access", feature: true do
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
@@ -225,6 +256,8 @@ describe "Internal Project Access", feature: true do
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
@@ -236,6 +269,8 @@ describe "Internal Project Access", feature: true do
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 a1e111c6cab..19f287ce7a4 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -8,10 +8,12 @@ describe "Private Project Access", feature: true do
let(:master) { create(:user) }
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
+ let(:external_team_member) { create(:user, external: true) }
before do
# full access
project.team << [master, :master]
+ project.team << [external_team_member, :master]
# readonly
project.team << [reporter, :reporter]
@@ -34,6 +36,8 @@ describe "Private Project Access", feature: true do
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
@@ -45,6 +49,8 @@ describe "Private Project Access", feature: true do
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
@@ -56,6 +62,8 @@ describe "Private Project Access", feature: true do
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
@@ -67,6 +75,7 @@ describe "Private Project Access", feature: true do
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 :visitor }
end
@@ -78,6 +87,8 @@ describe "Private Project Access", feature: true do
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
@@ -89,22 +100,23 @@ describe "Private Project Access", feature: true do
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/blob" do
- before do
- commit = project.repository.commit
- path = '.gitignore'
- @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path))
- end
+ let(:commit) { project.repository.commit }
+ subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))}
- it { expect(@blob_path).to be_allowed_for master }
- it { expect(@blob_path).to be_allowed_for reporter }
- it { expect(@blob_path).to be_allowed_for :admin }
- it { expect(@blob_path).to be_denied_for guest }
- it { expect(@blob_path).to be_denied_for :user }
- it { expect(@blob_path).to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for master }
+ 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
@@ -115,6 +127,8 @@ describe "Private Project Access", feature: true do
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
@@ -126,6 +140,8 @@ describe "Private Project Access", feature: true do
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
@@ -137,6 +153,8 @@ describe "Private Project Access", feature: true do
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
@@ -149,6 +167,8 @@ describe "Private Project Access", feature: true do
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
@@ -160,6 +180,8 @@ describe "Private Project Access", feature: true do
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
@@ -171,6 +193,8 @@ describe "Private Project Access", feature: true do
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
@@ -187,6 +211,8 @@ describe "Private Project Access", feature: true do
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
@@ -203,6 +229,8 @@ describe "Private Project Access", feature: true do
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
@@ -214,6 +242,8 @@ describe "Private Project Access", feature: true do
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 655d2c8b7d9..4e135076367 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -38,6 +38,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -49,6 +50,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -60,6 +62,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -71,6 +74,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -82,6 +86,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -93,22 +98,79 @@ describe "Public Project Access", feature: true do
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_denied_for :visitor }
end
- describe "GET /:project_path/blob" do
- before do
- commit = project.repository.commit
- path = '.gitignore'
- @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path))
+ describe "GET /:project_path/builds" do
+ subject { namespace_project_builds_path(project.namespace, project) }
+
+ context "when allowed for public" do
+ before { project.update(public_builds: true) }
+
+ it { is_expected.to be_allowed_for master }
+ 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 }
+ it { is_expected.to be_allowed_for :visitor }
+ end
+
+ context "when disallowed for public" do
+ before { project.update(public_builds: false) }
+
+ it { is_expected.to be_allowed_for master }
+ 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_denied_for :visitor }
+ end
+ end
+
+ describe "GET /:project_path/builds/:id" do
+ let(:commit) { create(:ci_commit, project: project) }
+ let(:build) { create(:ci_build, commit: commit) }
+ subject { namespace_project_build_path(project.namespace, project, build.id) }
+
+ context "when allowed for public" do
+ before { project.update(public_builds: true) }
+
+ it { is_expected.to be_allowed_for master }
+ 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 }
+ it { is_expected.to be_allowed_for :visitor }
end
- it { expect(@blob_path).to be_allowed_for master }
- it { expect(@blob_path).to be_allowed_for reporter }
- it { expect(@blob_path).to be_allowed_for :admin }
- it { expect(@blob_path).to be_allowed_for guest }
- it { expect(@blob_path).to be_allowed_for :user }
- it { expect(@blob_path).to be_allowed_for :visitor }
+ context "when disallowed for public" do
+ before { project.update(public_builds: false) }
+
+ it { is_expected.to be_allowed_for master }
+ 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_denied_for :visitor }
+ end
+ end
+
+ describe "GET /:project_path/blob" 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 master }
+ 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 }
end
describe "GET /:project_path/edit" do
@@ -119,6 +181,7 @@ describe "Public Project Access", feature: true do
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_denied_for :visitor }
end
@@ -130,6 +193,7 @@ describe "Public Project Access", feature: true do
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_denied_for :visitor }
end
@@ -141,6 +205,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -153,6 +218,7 @@ describe "Public Project Access", feature: true do
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_denied_for :visitor }
end
@@ -164,6 +230,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -175,6 +242,7 @@ describe "Public Project Access", feature: true do
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_denied_for :visitor }
end
@@ -186,6 +254,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -197,6 +266,7 @@ describe "Public Project Access", feature: true do
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_denied_for :visitor }
end
@@ -213,6 +283,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -229,6 +300,7 @@ describe "Public Project Access", feature: true do
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 }
it { is_expected.to be_allowed_for :visitor }
end
@@ -240,6 +312,7 @@ describe "Public Project Access", feature: true do
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_denied_for :visitor }
end
end
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
deleted file mode 100644
index 4f6a000822e..00000000000
--- a/spec/finders/groups_finder_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe GroupsFinder do
- describe '#execute' do
- let(:user) { create(:user) }
-
- let(:group1) { create(:group) }
- let(:group2) { create(:group) }
- let(:group3) { create(:group) }
- let(:group4) { create(:group, public: true) }
-
- let!(:public_project) { create(:project, :public, group: group1) }
- let!(:internal_project) { create(:project, :internal, group: group2) }
- let!(:private_project) { create(:project, :private, group: group3) }
-
- let(:finder) { described_class.new }
-
- describe 'with a user' do
- subject { finder.execute(user) }
-
- describe 'when the user is not a member of any groups' do
- it { is_expected.to eq([group4, group2, group1]) }
- end
-
- describe 'when the user is a member of a group' do
- before do
- group3.add_user(user, Gitlab::Access::DEVELOPER)
- end
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
-
- describe 'when the user is a member of a private project' do
- before do
- private_project.team.add_user(user, Gitlab::Access::DEVELOPER)
- end
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
- end
-
- describe 'without a user' do
- subject { finder.execute }
-
- it { is_expected.to eq([group4, group1]) }
- end
- end
-end
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
deleted file mode 100644
index 2d9068cc720..00000000000
--- a/spec/finders/joined_groups_finder_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'spec_helper'
-
-describe JoinedGroupsFinder do
- describe '#execute' do
- let(:source_user) { create(:user) }
- let(:current_user) { create(:user) }
-
- let(:group1) { create(:group) }
- let(:group2) { create(:group) }
- let(:group3) { create(:group) }
- let(:group4) { create(:group, public: true) }
-
- let!(:public_project) { create(:project, :public, group: group1) }
- let!(:internal_project) { create(:project, :internal, group: group2) }
- let!(:private_project) { create(:project, :private, group: group3) }
-
- let(:finder) { described_class.new(source_user) }
-
- before do
- [group1, group2, group3, group4].each do |group|
- group.add_user(source_user, Gitlab::Access::MASTER)
- end
- end
-
- describe 'with a current user' do
- describe 'when the current user has access to the projects of the source user' do
- before do
- private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER)
- end
-
- subject { finder.execute(current_user) }
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
-
- describe 'when the current user does not have access to the projects of the source user' do
- subject { finder.execute(current_user) }
-
- it { is_expected.to eq([group4, group2, group1]) }
- end
- end
-
- describe 'without a current user' do
- subject { finder.execute }
-
- it { is_expected.to eq([group4, group1]) }
- end
- end
-end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index f32641ef0f6..fae0da9d898 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -17,6 +17,10 @@ describe ProjectsFinder do
create(:project, :public, group: group, name: 'C', path: 'C')
end
+ let!(:shared_project) do
+ create(:project, :private, name: 'D', path: 'D')
+ end
+
let(:finder) { described_class.new }
describe 'without a group' do
@@ -56,7 +60,35 @@ describe ProjectsFinder do
describe 'with a user' do
subject { finder.execute(user, group: group) }
- it { is_expected.to eq([public_project, internal_project]) }
+ describe 'without shared projects' do
+ it { is_expected.to eq([public_project, internal_project]) }
+ end
+
+ describe 'with shared projects and group membership' do
+ before do
+ group.add_user(user, Gitlab::Access::DEVELOPER)
+
+ shared_project.project_group_links.
+ create(group_access: Gitlab::Access::MASTER, group: group)
+ end
+
+ it do
+ is_expected.to eq([shared_project, public_project, internal_project])
+ end
+ end
+
+ describe 'with shared projects and project membership' do
+ before do
+ shared_project.team.add_user(user, Gitlab::Access::DEVELOPER)
+
+ shared_project.project_group_links.
+ create(group_access: Gitlab::Access::MASTER, group: group)
+ end
+
+ it do
+ is_expected.to eq([shared_project, public_project, internal_project])
+ end
+ end
end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 1b4ffc2d717..7fdc5e5d7aa 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -5,15 +5,14 @@ describe SnippetsFinder do
let(:user1) { create :user }
let(:group) { create :group }
- let(:project1) { create(:empty_project, :public, group: group) }
- let(:project2) { create(:empty_project, :private, group: group) }
-
+ let(:project1) { create(:empty_project, :public, group: group) }
+ let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do
before do
- @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE)
- @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL)
- @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC)
+ @snippet1 = create(:personal_snippet, :private)
+ @snippet2 = create(:personal_snippet, :internal)
+ @snippet3 = create(:personal_snippet, :public)
end
it "returns all private and internal snippets" do
@@ -31,9 +30,9 @@ describe SnippetsFinder do
context ':by_user filter' do
before do
- @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user)
- @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user)
- @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user)
+ @snippet1 = create(:personal_snippet, :private, author: user)
+ @snippet2 = create(:personal_snippet, :internal, author: user)
+ @snippet3 = create(:personal_snippet, :public, author: user)
end
it "returns all public and internal snippets" do
@@ -75,9 +74,9 @@ describe SnippetsFinder do
context 'by_project filter' do
before do
- @snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1)
- @snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1)
- @snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1)
+ @snippet1 = create(:project_snippet, :private, project: project1)
+ @snippet2 = create(:project_snippet, :internal, project: project1)
+ @snippet3 = create(:project_snippet, :public, project: project1)
end
it "returns public snippets for unauthorized user" do
@@ -93,7 +92,7 @@ describe SnippetsFinder do
end
it "returns all snippets for project members" do
- project1.team << [user, :developer]
+ project1.team << [user, :developer]
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
diff --git a/spec/fixtures/ci_build_artifacts.zip b/spec/fixtures/ci_build_artifacts.zip
new file mode 100644
index 00000000000..dae976d918e
--- /dev/null
+++ b/spec/fixtures/ci_build_artifacts.zip
Binary files differ
diff --git a/spec/fixtures/ci_build_artifacts_metadata.gz b/spec/fixtures/ci_build_artifacts_metadata.gz
new file mode 100644
index 00000000000..e6d17e4595d
--- /dev/null
+++ b/spec/fixtures/ci_build_artifacts_metadata.gz
Binary files differ
diff --git a/public/logo.svg b/spec/fixtures/logo_sample.svg
index c09785cb96f..883e7e6cf92 100644
--- a/public/logo.svg
+++ b/spec/fixtures/logo_sample.svg
@@ -3,6 +3,7 @@
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
+ <script>alert('FAIL')</script>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)">
@@ -10,17 +11,17 @@
<g id="Fill-1-+-Group-24">
<g id="Group-24">
<g id="Group">
- <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path>
- <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path>
- <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path>
- <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path>
- <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path>
- <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path>
- <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path>
+ <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329" class="tanuki-shape"></path>
+ <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26" class="tanuki-shape"></path>
+ <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326" class="tanuki-shape"></path>
+ <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329" class="tanuki-shape"></path>
+ <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26" class="tanuki-shape"></path>
+ <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326" class="tanuki-shape"></path>
+ <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329" class="tanuki-shape"></path>
</g>
</g>
</g>
</g>
</g>
</g>
-</svg> \ No newline at end of file
+</svg>
diff --git a/spec/fixtures/mail_room_disabled.yml b/spec/fixtures/mail_room_disabled.yml
new file mode 100644
index 00000000000..97f8cff051f
--- /dev/null
+++ b/spec/fixtures/mail_room_disabled.yml
@@ -0,0 +1,11 @@
+test:
+ incoming_email:
+ enabled: false
+ address: "gitlab-incoming+%{key}@gmail.com"
+ user: "gitlab-incoming@gmail.com"
+ password: "[REDACTED]"
+ host: "imap.gmail.com"
+ port: 993
+ ssl: true
+ start_tls: false
+ mailbox: "inbox"
diff --git a/spec/fixtures/mail_room_enabled.yml b/spec/fixtures/mail_room_enabled.yml
new file mode 100644
index 00000000000..9c94649244d
--- /dev/null
+++ b/spec/fixtures/mail_room_enabled.yml
@@ -0,0 +1,11 @@
+test:
+ incoming_email:
+ enabled: true
+ address: "gitlab-incoming+%{key}@gmail.com"
+ user: "gitlab-incoming@gmail.com"
+ password: "[REDACTED]"
+ host: "imap.gmail.com"
+ port: 993
+ ssl: true
+ start_tls: false
+ mailbox: "inbox"
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index e8dfc5c0eb1..1772cc3f6a4 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -209,11 +209,18 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Label by ID: <%= simple_label.to_reference %>
- Label by name: <%= Label.reference_prefix %><%= simple_label.name %>
-- Label by name in quotes: <%= label.to_reference(:name) %>
+- Label by name in quotes: <%= label.to_reference(format: :name) %>
- Ignored in code: `<%= simple_label.to_reference %>`
- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
- Link to label by reference: [Label](<%= label.to_reference %>)
+#### MilestoneReferenceFilter
+
+- Milestone: <%= milestone.to_reference %>
+- Milestone in another project: <%= xmilestone.to_reference(project) %>
+- Ignored in code: `<%= milestone.to_reference %>`
+- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>)
+
### Task Lists
- [ ] Incomplete task 1
@@ -223,3 +230,12 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [ ] Incomplete sub-task 2
- [x] Complete sub-task 1
- [X] Complete task 2
+
+#### Gollum Tags
+
+- [[linked-resource]]
+- [[link-text|linked-resource]]
+- [[http://example.com]]
+- [[link-text|http://example.com/pdfs/gollum.pdf]]
+- [[images/example.jpg]]
+- [[http://example.com/images/example.jpg]]
diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml
new file mode 100644
index 00000000000..a8b7907d4ba
--- /dev/null
+++ b/spec/fixtures/parallel_diff_result.yml
@@ -0,0 +1,324 @@
+---
+- :left:
+ :type: match
+ :number: 6
+ :text: "@@ -6,12 +6,18 @@ module Popen"
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
+ :right:
+ :type: match
+ :number: 6
+ :text: "@@ -6,12 +6,18 @@ module Popen"
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
+- :left:
+ :type:
+ :number: 6
+ :text: |2
+ <span id="LC6" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
+ :right:
+ :type:
+ :number: 6
+ :text: |2
+ <span id="LC6" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
+- :left:
+ :type:
+ :number: 7
+ :text: |2
+ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7
+ :right:
+ :type:
+ :number: 7
+ :text: |2
+ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7
+- :left:
+ :type:
+ :number: 8
+ :text: |2
+ <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8
+ :right:
+ :type:
+ :number: 8
+ :text: |2
+ <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8
+- :left:
+ :type: old
+ :number: 9
+ :text: |
+ -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9
+ :right:
+ :type: new
+ :number: 9
+ :text: |
+ +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9
+- :left:
+ :type:
+ :number: 10
+ :text: |2
+ <span id="LC10" class="line"> <span class="k">end</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10
+ :right:
+ :type:
+ :number: 10
+ :text: |2
+ <span id="LC10" class="line"> <span class="k">end</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10
+- :left:
+ :type:
+ :number: 11
+ :text: |2
+ <span id="LC11" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11
+ :right:
+ :type:
+ :number: 11
+ :text: |2
+ <span id="LC11" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11
+- :left:
+ :type:
+ :number: 12
+ :text: |2
+ <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12
+ :right:
+ :type:
+ :number: 12
+ :text: |2
+ <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12
+- :left:
+ :type: old
+ :number: 13
+ :text: |
+ -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&quot;PWD&quot;</span> <span class="o">=&gt;</span> <span class="n">path</span> <span class="p">}</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13
+ :right:
+ :type: old
+ :number:
+ :text: ''
+ :line_code:
+- :left:
+ :type: old
+ :number: 14
+ :text: |
+ -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13
+ :right:
+ :type: new
+ :number: 13
+ :text: |
+ +<span id="LC13" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13
+- :left:
+ :type:
+ :number:
+ :text: ''
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14
+ :right:
+ :type: new
+ :number: 14
+ :text: |
+ +<span id="LC14" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14
+- :left:
+ :type:
+ :number:
+ :text: ''
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15
+ :right:
+ :type: new
+ :number: 15
+ :text: |
+ +<span id="LC15" class="line"> <span class="s2">&quot;PWD&quot;</span> <span class="o">=&gt;</span> <span class="n">path</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15
+- :left:
+ :type:
+ :number:
+ :text: ''
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16
+ :right:
+ :type: new
+ :number: 16
+ :text: |
+ +<span id="LC16" class="line"> <span class="p">}</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16
+- :left:
+ :type:
+ :number:
+ :text: ''
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17
+ :right:
+ :type: new
+ :number: 17
+ :text: |
+ +<span id="LC17" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17
+- :left:
+ :type:
+ :number:
+ :text: ''
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18
+ :right:
+ :type: new
+ :number: 18
+ :text: |
+ +<span id="LC18" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18
+- :left:
+ :type:
+ :number:
+ :text: ''
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19
+ :right:
+ :type: new
+ :number: 19
+ :text: |
+ +<span id="LC19" class="line"> <span class="ss">chdir: </span><span class="n">path</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19
+- :left:
+ :type:
+ :number:
+ :text: ''
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20
+ :right:
+ :type: new
+ :number: 20
+ :text: |
+ +<span id="LC20" class="line"> <span class="p">}</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20
+- :left:
+ :type:
+ :number: 15
+ :text: |2
+ <span id="LC21" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21
+ :right:
+ :type:
+ :number: 21
+ :text: |2
+ <span id="LC21" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21
+- :left:
+ :type:
+ :number: 16
+ :text: |2
+ <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22
+ :right:
+ :type:
+ :number: 22
+ :text: |2
+ <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22
+- :left:
+ :type:
+ :number: 17
+ :text: |2
+ <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23
+ :right:
+ :type:
+ :number: 23
+ :text: |2
+ <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23
+- :left:
+ :type: match
+ :number: 19
+ :text: "@@ -19,6 +25,7 @@ module Popen"
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
+ :right:
+ :type: match
+ :number: 25
+ :text: "@@ -19,6 +25,7 @@ module Popen"
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
+- :left:
+ :type:
+ :number: 19
+ :text: |2
+ <span id="LC25" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
+ :right:
+ :type:
+ :number: 25
+ :text: |2
+ <span id="LC25" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
+- :left:
+ :type:
+ :number: 20
+ :text: |2
+ <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26
+ :right:
+ :type:
+ :number: 26
+ :text: |2
+ <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26
+- :left:
+ :type:
+ :number: 21
+ :text: |2
+ <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27
+ :right:
+ :type:
+ :number: 27
+ :text: |2
+ <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27
+- :left:
+ :type:
+ :number:
+ :text: ''
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28
+ :right:
+ :type: new
+ :number: 28
+ :text: |
+ +<span id="LC28" class="line"></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28
+- :left:
+ :type:
+ :number: 22
+ :text: |2
+ <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29
+ :right:
+ :type:
+ :number: 29
+ :text: |2
+ <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29
+- :left:
+ :type:
+ :number: 23
+ :text: |2
+ <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30
+ :right:
+ :type:
+ :number: 30
+ :text: |2
+ <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30
+- :left:
+ :type:
+ :number: 24
+ :text: |2
+ <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31
+ :right:
+ :type:
+ :number: 31
+ :text: |2
+ <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span>
+ :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 68527c3a4f8..f6c1005d265 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -240,7 +240,7 @@ describe ApplicationHelper do
describe 'time_ago_with_tooltip' do
def element(*arguments)
Time.zone = 'UTC'
- time = Time.zone.parse('2015-07-02 08:00')
+ time = Time.zone.parse('2015-07-02 08:23')
element = helper.time_ago_with_tooltip(time, *arguments)
Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
@@ -251,15 +251,15 @@ describe ApplicationHelper do
end
it 'includes the date string' do
- expect(element.text).to eq '2015-07-02 08:00:00 UTC'
+ expect(element.text).to eq '2015-07-02 08:23:00 UTC'
end
it 'has a datetime attribute' do
- expect(element.attr('datetime')).to eq '2015-07-02T08:00:00Z'
+ expect(element.attr('datetime')).to eq '2015-07-02T08:23:00Z'
end
it 'has a formatted title attribute' do
- expect(element.attr('title')).to eq 'Jul 02, 2015 8:00am'
+ expect(element.attr('title')).to eq 'Jul 2, 2015 8:23am'
end
it 'includes a default js-timeago class' do
@@ -285,10 +285,18 @@ describe ApplicationHelper do
it 'allows the script tag to be excluded' do
expect(element(skip_js: true)).not_to include 'script'
end
+
+ it 'converts to Time' do
+ expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error
+ end
end
describe 'render_markup' do
let(:content) { 'Noël' }
+ let(:user) { create(:user) }
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
it 'should preserve encoding' do
expect(content.encoding.name).to eq('UTF-8')
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index b8bba36439a..87849230dbe 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -1,22 +1,22 @@
require 'spec_helper'
describe BlobHelper do
- describe 'highlight' do
- let(:blob_name) { 'test.lisp' }
- let(:no_context_content) { ":type \"assem\"))" }
- let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" }
- let(:split_content) { blob_content.split("\n") }
- let(:multiline_content) do
- %q(
- def test(input):
- """This is line 1 of a multi-line comment.
- This is line 2.
- """
- )
- end
+ let(:blob_name) { 'test.lisp' }
+ let(:no_context_content) { ":type \"assem\"))" }
+ let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" }
+ let(:split_content) { blob_content.split("\n") }
+ let(:multiline_content) do
+ %q(
+ def test(input):
+ """This is line 1 of a multi-line comment.
+ This is line 2.
+ """
+ )
+ end
+ describe '#highlight' do
it 'should return plaintext for unknown lexer context' do
- result = highlight(blob_name, no_context_content, nowrap: true, continue: false)
+ result = helper.highlight(blob_name, no_context_content, nowrap: true)
expect(result).to eq('<span id="LC1" class="line">:type &quot;assem&quot;))</span>')
end
@@ -24,28 +24,17 @@ describe BlobHelper do
expected = %Q[<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">&quot;assem&quot;</span><span class="p">))</span></span>]
- expect(highlight(blob_name, blob_content, nowrap: true, continue: false)).to eq(expected)
- end
-
- it 'should highlight continued blocks' do
- # Both lines have LC1 as ID since formatter doesn't support continue at the moment
- expected = [
- '<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>',
- '<span id="LC1" class="line"><span class="ss">:type</span> <span class="s">&quot;assem&quot;</span><span class="p">))</span></span>'
- ]
-
- result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) }
- expect(result).to eq(expected)
+ expect(helper.highlight(blob_name, blob_content, nowrap: true)).to eq(expected)
end
it 'should highlight multi-line comments' do
- result = highlight(blob_name, multiline_content, nowrap: true, continue: false)
+ result = helper.highlight(blob_name, multiline_content, nowrap: true)
html = Nokogiri::HTML(result)
lines = html.search('.s')
expect(lines.count).to eq(3)
expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.')
- expect(lines[1].text).to eq(' This is line 2.')
- expect(lines[2].text).to eq(' """')
+ expect(lines[1].text).to eq(' This is line 2.')
+ expect(lines[2].text).to eq(' """')
end
context 'diff highlighting' do
@@ -59,9 +48,23 @@ describe BlobHelper do
end
it 'should highlight each line properly' do
- result = highlight(blob_name, blob_content, nowrap: true, continue: false)
+ result = helper.highlight(blob_name, blob_content, nowrap: true)
expect(result).to eq(expected)
end
end
end
+
+ describe "#highlighter" do
+ it 'should highlight continued blocks' do
+ # Both lines have LC1 as ID since formatter doesn't support continue at the moment
+ expected = [
+ '<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>',
+ '<span id="LC1" class="line"><span class="ss">:type</span> <span class="s">&quot;assem&quot;</span><span class="p">))</span></span>'
+ ]
+
+ highlighter = helper.highlighter(blob_name, blob_content, nowrap: true)
+ result = split_content.map{ |content| highlighter.highlight(content) }
+ expect(result).to eq(expected)
+ end
+ end
end
diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb
index c7c6f45d144..157cc4665a2 100644
--- a/spec/helpers/broadcast_messages_helper_spec.rb
+++ b/spec/helpers/broadcast_messages_helper_spec.rb
@@ -1,22 +1,60 @@
require 'spec_helper'
describe BroadcastMessagesHelper do
- describe 'broadcast_styling' do
- let(:broadcast_message) { double(color: '', font: '') }
+ describe 'broadcast_message' do
+ it 'returns nil when no current message' do
+ expect(helper.broadcast_message(nil)).to be_nil
+ end
+
+ it 'includes the current message' do
+ current = double(message: 'Current Message')
+
+ allow(helper).to receive(:broadcast_message_style).and_return(nil)
+
+ expect(helper.broadcast_message(current)).to include 'Current Message'
+ end
+
+ it 'includes custom style' do
+ current = double(message: 'Current Message')
+
+ allow(helper).to receive(:broadcast_message_style).and_return('foo')
+
+ expect(helper.broadcast_message(current)).to include 'style="foo"'
+ end
+ end
+
+ describe 'broadcast_message_style' do
+ it 'defaults to no style' do
+ broadcast_message = spy
+
+ expect(helper.broadcast_message_style(broadcast_message)).to eq ''
+ end
+
+ it 'allows custom style' do
+ broadcast_message = double(color: '#f2dede', font: '#b94a48')
+
+ expect(helper.broadcast_message_style(broadcast_message)).
+ to match('background-color: #f2dede; color: #b94a48')
+ end
+ end
+
+ describe 'broadcast_message_status' do
+ it 'returns Active' do
+ message = build(:broadcast_message)
+
+ expect(helper.broadcast_message_status(message)).to eq 'Active'
+ end
+
+ it 'returns Expired' do
+ message = build(:broadcast_message, :expired)
- context "default style" do
- it "should have no style" do
- expect(broadcast_styling(broadcast_message)).to eq ''
- end
+ expect(helper.broadcast_message_status(message)).to eq 'Expired'
end
- context "customized style" do
- let(:broadcast_message) { double(color: "#f2dede", font: '#b94a48') }
+ it 'returns Pending' do
+ message = build(:broadcast_message, :future)
- it "should have a customized style" do
- expect(broadcast_styling(broadcast_message)).
- to match('background-color: #f2dede; color: #b94a48')
- end
+ expect(helper.broadcast_message_status(message)).to eq 'Pending'
end
end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 7c96a74e581..982c113e84b 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -4,10 +4,12 @@ describe DiffHelper do
include RepoHelpers
let(:project) { create(:project) }
+ let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
let(:diffs) { commit.diffs }
let(:diff) { diffs.first }
- let(:diff_file) { Gitlab::Diff::File.new(diff) }
+ let(:diff_refs) { [commit.parent, commit] }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) }
describe 'diff_hard_limit_enabled?' do
it 'should return true if param is provided' do
@@ -20,79 +22,14 @@ describe DiffHelper do
end
end
- describe 'allowed_diff_size' do
+ describe 'diff_options' do
it 'should return hard limit for a diff if force diff is true' do
allow(controller).to receive(:params) { { force_show_diff: true } }
- expect(allowed_diff_size).to eq(1000)
+ expect(diff_options).to include(Commit.max_diff_options)
end
it 'should return safe limit for a diff if force diff is false' do
- expect(allowed_diff_size).to eq(100)
- end
- end
-
- describe 'allowed_diff_lines' do
- it 'should return hard limit for number of lines in a diff if force diff is true' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- expect(allowed_diff_lines).to eq(50000)
- end
-
- it 'should return safe limit for numbers of lines a diff if force diff is false' do
- expect(allowed_diff_lines).to eq(5000)
- end
- end
-
- describe 'safe_diff_files' do
- it 'should return all files from a commit that is smaller than safe limits' do
- expect(safe_diff_files(diffs).length).to eq(2)
- end
-
- it 'should return only the first file if the diff line count in the 2nd file takes the total beyond safe limits' do
- allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines
- expect(safe_diff_files(diffs).length).to eq(1)
- end
-
- it 'should return all files from a commit that is beyond safe limit for numbers of lines if force diff is true' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines
- expect(safe_diff_files(diffs).length).to eq(2)
- end
-
- it 'should return only the first file if the diff line count in the 2nd file takes the total beyond hard limits' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- allow(diffs[1].diff).to receive(:lines).and_return([""] * 49999) #simulate 49999 lines
- expect(safe_diff_files(diffs).length).to eq(1)
- end
-
- it 'should return only a safe number of file diffs if a commit touches more files than the safe limits' do
- large_diffs = diffs * 100 #simulate 200 diffs
- expect(safe_diff_files(large_diffs).length).to eq(100)
- end
-
- it 'should return all file diffs if a commit touches more files than the safe limits but force diff is true' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- large_diffs = diffs * 100 #simulate 200 diffs
- expect(safe_diff_files(large_diffs).length).to eq(200)
- end
-
- it 'should return a limited file diffs if a commit touches more files than the hard limits and force diff is true' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- very_large_diffs = diffs * 1000 #simulate 2000 diffs
- expect(safe_diff_files(very_large_diffs).length).to eq(1000)
- end
- end
-
- describe 'parallel_diff' do
- it 'should return an array of arrays containing the parsed diff' do
- expect(parallel_diff(diff_file, 0)).
- to match_array(parallel_diff_result_array)
- end
- end
-
- describe 'generate_line_code' do
- it 'should generate correct line code' do
- expect(generate_line_code(diff_file.file_path, diff_file.diff_lines.first)).
- to eq('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6')
+ expect(diff_options).not_to include(:max_lines, :max_files)
end
end
@@ -116,8 +53,7 @@ describe DiffHelper do
end
end
- describe 'diff_line_content' do
-
+ describe '#diff_line_content' do
it 'should return non breaking space when line is empty' do
expect(diff_line_content(nil)).to eq(' &nbsp;')
end
@@ -126,39 +62,21 @@ describe DiffHelper do
expect(diff_line_content(diff_file.diff_lines.first.text)).
to eq('@@ -6,12 +6,18 @@ module Popen')
expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match')
- expect(diff_line_content(diff_file.diff_lines.first.new_pos)).to eq(6)
+ expect(diff_file.diff_lines.first.new_pos).to eq(6)
end
end
- def parallel_diff_result_array
- [
- ["match", 6, "@@ -6,12 +6,18 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6", "match", 6, "@@ -6,12 +6,18 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6"],
- [nil, 6, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6", nil, 6, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6"], [nil, 7, " def popen(cmd, path=nil)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7", nil, 7, " def popen(cmd, path=nil)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"],
- [nil, 8, " unless cmd.is_a?(Array)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8", nil, 8, " unless cmd.is_a?(Array)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"],
- ["old", 9, "- raise <span class='idiff'></span>&quot;System commands must be given as an array of strings&quot;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9", "new", 9, "+ raise <span class='idiff'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"],
- [nil, 10, " end", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10", nil, 10, " end", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"],
- [nil, 11, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11", nil, 11, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11"],
- [nil, 12, " path ||= Dir.pwd", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12", nil, 12, " path ||= Dir.pwd", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12"],
- ["old", 13, "- vars = { &quot;PWD&quot; =&gt; path }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13", "old", nil, "&nbsp;", nil],
- ["old", 14, "- options = { chdir: path }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13", "new", 13, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13"],
- [nil, nil, "&nbsp;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14", "new", 14, "+ vars = {", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14"],
- [nil, nil, "&nbsp;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15", "new", 15, "+ &quot;PWD&quot; =&gt; path", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"],
- [nil, nil, "&nbsp;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16", "new", 16, "+ }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16"],
- [nil, nil, "&nbsp;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17", "new", 17, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17"],
- [nil, nil, "&nbsp;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18", "new", 18, "+ options = {", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18"],
- [nil, nil, "&nbsp;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19", "new", 19, "+ chdir: path", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19"],
- [nil, nil, "&nbsp;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20", "new", 20, "+ }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20"],
- [nil, 15, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21", nil, 21, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21"],
- [nil, 16, " unless File.directory?(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22", nil, 22, " unless File.directory?(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22"],
- [nil, 17, " FileUtils.mkdir_p(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23", nil, 23, " FileUtils.mkdir_p(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23"],
- ["match", 19, "@@ -19,6 +25,7 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25", "match", 25, "@@ -19,6 +25,7 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25"],
- [nil, 19, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25", nil, 25, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25"],
- [nil, 20, " @cmd_output = &quot;&quot;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26", nil, 26, " @cmd_output = &quot;&quot;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26"],
- [nil, 21, " @cmd_status = 0", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27", nil, 27, " @cmd_status = 0", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27"],
- [nil, nil, "&nbsp;", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28", "new", 28, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28"],
- [nil, 22, " Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29", nil, 29, " Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29"],
- [nil, 23, " @cmd_output &lt;&lt; stdout.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30", nil, 30, " @cmd_output &lt;&lt; stdout.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30"],
- [nil, 24, " @cmd_output &lt;&lt; stderr.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31", nil, 31, " @cmd_output &lt;&lt; stderr.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31"]
- ]
+ describe "#mark_inline_diffs" do
+ let(:old_line) { %{abc 'def'} }
+ let(:new_line) { %{abc "def"} }
+
+ it "returns strings with marked inline diffs" do
+ marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
+
+ expect(marked_old_line).to eq("abc <span class='idiff left right'>&#39;def&#39;</span>")
+ expect(marked_old_line).to be_html_safe
+ expect(marked_new_line).to eq("abc <span class='idiff left right'>&quot;def&quot;</span>")
+ expect(marked_new_line).to be_html_safe
+ end
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 762ec25c4f5..9adcd916ced 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do
it 'should replace commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo')
expect(actual).
- to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/emoji/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
+ to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
end
end
@@ -121,12 +121,13 @@ describe GitlabMarkdownHelper do
before do
@wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content')
+ helper.instance_variable_set(:@project_wiki, @wiki)
end
- it "should use GitLab Flavored Markdown for markdown files" do
+ it "should use Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
- expect(helper).to receive(:markdown).with('wiki content')
+ expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki)
helper.render_wiki_content(@wiki)
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 0c8d06b7059..4f129eca183 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe LabelsHelper do
describe 'link_to_label' do
let(:project) { create(:empty_project) }
- let(:label) { create(:label, project: project) }
+ let(:label) { create(:label, project: project) }
context 'with @project set' do
before do
@@ -11,34 +11,31 @@ describe LabelsHelper do
end
it 'uses the instance variable' do
- expect(label).not_to receive(:project)
- link_to_label(label)
+ expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name=#{label.name}">.*</a>}
end
end
context 'without @project set' do
it "uses the label's project" do
- expect(label).to receive(:project).and_return(project)
- link_to_label(label)
+ expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name=#{label.name}">.*</a>}
end
end
- context 'with a named project argument' do
- it 'uses the provided project' do
- arg = double('project')
- expect(arg).to receive(:namespace).and_return('foo')
- expect(arg).to receive(:to_param).and_return('foo')
+ context 'with a project argument' do
+ let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') }
- link_to_label(label, project: arg)
+ it 'links to merge requests page' do
+ expect(link_to_label(label, project: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name=#{label.name}">.*</a>}
end
+ end
- it 'takes precedence over other types' do
- @project = project
- expect(@project).not_to receive(:namespace)
- expect(label).not_to receive(:project)
-
- arg = double('project', namespace: 'foo', to_param: 'foo')
- link_to_label(label, project: arg)
+ context 'with a type argument' do
+ ['issue', :issue, 'merge_request', :merge_request].each do |type|
+ context "set to #{type}" do
+ it 'links to correct page' do
+ expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.to_reference}/#{type.to_s.pluralize}\?label_name=#{label.name}">.*</a>}
+ end
+ end
end
end
@@ -66,5 +63,10 @@ describe LabelsHelper do
it 'uses dark text on light backgrounds' do
expect(text_color_for_bg('#EEEEEE')).to eq('#333333')
end
+
+ it 'supports RGB triplets' do
+ expect(text_color_for_bg('#FFF')).to eq '#333333'
+ expect(text_color_for_bg('#000')).to eq '#FFFFFF'
+ end
end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index fd7107779f6..cf632f594c7 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -2,10 +2,8 @@ require 'rails_helper'
describe PageLayoutHelper do
describe 'page_description' do
- it 'defaults to value returned by page_description_default helper' do
- allow(helper).to receive(:page_description_default).and_return('Foo')
-
- expect(helper.page_description).to eq 'Foo'
+ it 'defaults to nil' do
+ expect(helper.page_description).to eq nil
end
it 'returns the last-pushed description' do
@@ -42,58 +40,32 @@ describe PageLayoutHelper do
end
end
- describe 'page_description_default' do
- it 'uses Project description when available' do
- project = double(description: 'Project Description')
- helper.instance_variable_set(:@project, project)
-
- expect(helper.page_description_default).to eq 'Project Description'
- end
-
- it 'uses brand_title when Project description is nil' do
- project = double(description: nil)
- helper.instance_variable_set(:@project, project)
-
- expect(helper).to receive(:brand_title).and_return('Brand Title')
- expect(helper.page_description_default).to eq 'Brand Title'
- end
-
- it 'falls back to brand_title' do
- allow(helper).to receive(:brand_title).and_return('Brand Title')
-
- expect(helper.page_description_default).to eq 'Brand Title'
- end
- end
-
describe 'page_image' do
it 'defaults to the GitLab logo' do
expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
end
- context 'with @project' do
- it 'uses Project avatar if available' do
- project = double(avatar_url: 'http://example.com/uploads/avatar.png')
- helper.instance_variable_set(:@project, project)
+ %w(project user group).each do |type|
+ context "with @#{type} assigned" do
+ it "uses #{type.titlecase} avatar if available" do
+ object = double(avatar_url: 'http://example.com/uploads/avatar.png')
+ assign(type, object)
- expect(helper.page_image).to eq project.avatar_url
- end
+ expect(helper.page_image).to eq object.avatar_url
+ end
- it 'falls back to the default' do
- project = double(avatar_url: nil)
- helper.instance_variable_set(:@project, project)
+ it 'falls back to the default when avatar_url is nil' do
+ object = double(avatar_url: nil)
+ assign(type, object)
- expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ end
end
- end
-
- context 'with @user' do
- it 'delegates to avatar_icon helper' do
- user = double('User')
- helper.instance_variable_set(:@user, user)
-
- expect(helper).to receive(:avatar_icon).with(user)
- helper.page_image
+ context "with no assignments" do
+ it 'falls back to the default' do
+ expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ end
end
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index ebe9c29d91c..601b6915e27 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -42,9 +42,9 @@ describe SearchHelper do
expect(search_autocomplete_opts(project.name).size).to eq(1)
end
- it "includes the public group" do
- group = create(:group, public: true)
- expect(search_autocomplete_opts(group.name).size).to eq(1)
+ it "should not include the public group" do
+ group = create(:group)
+ expect(search_autocomplete_opts(group.name).size).to eq(0)
end
context "with a current project" do
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index aafc24397a9..cd7596a763d 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -58,7 +58,7 @@ describe VisibilityLevelHelper do
describe "skip_level?" do
describe "forks" do
- let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+ let(:project) { create(:project, :internal) }
let(:fork_project) { create(:forked_project_with_submodules) }
before do
@@ -74,7 +74,7 @@ describe VisibilityLevelHelper do
end
describe "non-forked project" do
- let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+ let(:project) { create(:project, :internal) }
it "skips levels" do
expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
@@ -84,7 +84,7 @@ describe VisibilityLevelHelper do
end
describe "Snippet" do
- let(:snippet) { create(:snippet, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+ let(:snippet) { create(:snippet, :internal) }
it "skips levels" do
expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
new file mode 100644
index 00000000000..e58f2c80e95
--- /dev/null
+++ b/spec/initializers/settings_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../config/initializers/1_settings'
+
+describe Settings, lib: true do
+
+ describe '#host_without_www' do
+ context 'URL with protocol' do
+ it 'returns the host' do
+ expect(Settings.host_without_www('http://foo.com')).to eq 'foo.com'
+ expect(Settings.host_without_www('http://www.foo.com')).to eq 'foo.com'
+ expect(Settings.host_without_www('http://secure.foo.com')).to eq 'secure.foo.com'
+ expect(Settings.host_without_www('http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
+
+ expect(Settings.host_without_www('https://foo.com')).to eq 'foo.com'
+ expect(Settings.host_without_www('https://www.foo.com')).to eq 'foo.com'
+ expect(Settings.host_without_www('https://secure.foo.com')).to eq 'secure.foo.com'
+ expect(Settings.host_without_www('https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'secure.gravatar.com'
+ end
+ end
+
+ context 'URL without protocol' do
+ it 'returns the host' do
+ expect(Settings.host_without_www('foo.com')).to eq 'foo.com'
+ expect(Settings.host_without_www('www.foo.com')).to eq 'foo.com'
+ expect(Settings.host_without_www('secure.foo.com')).to eq 'secure.foo.com'
+ expect(Settings.host_without_www('www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
+ end
+
+ context 'URL with user/port' do
+ it 'returns the host' do
+ expect(Settings.host_without_www('bob:pass@foo.com:8080')).to eq 'foo.com'
+ expect(Settings.host_without_www('bob:pass@www.foo.com:8080')).to eq 'foo.com'
+ expect(Settings.host_without_www('bob:pass@secure.foo.com:8080')).to eq 'secure.foo.com'
+ expect(Settings.host_without_www('bob:pass@www.gravatar.com:8080/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
+
+ expect(Settings.host_without_www('http://bob:pass@foo.com:8080')).to eq 'foo.com'
+ expect(Settings.host_without_www('http://bob:pass@www.foo.com:8080')).to eq 'foo.com'
+ expect(Settings.host_without_www('http://bob:pass@secure.foo.com:8080')).to eq 'secure.foo.com'
+ expect(Settings.host_without_www('http://bob:pass@www.gravatar.com:8080/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
+ end
+ end
+ end
+ end
+
+end
diff --git a/spec/javascripts/behaviors/autosize_spec.js.coffee b/spec/javascripts/behaviors/autosize_spec.js.coffee
new file mode 100644
index 00000000000..7fc1d19c35f
--- /dev/null
+++ b/spec/javascripts/behaviors/autosize_spec.js.coffee
@@ -0,0 +1,11 @@
+#= require behaviors/autosize
+
+describe 'Autosize behavior', ->
+ beforeEach ->
+ fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>')
+
+ it 'does not overwrite the resize property', ->
+ load()
+ expect($('textarea')).toHaveCss(resize: 'vertical')
+
+ load = -> $(document).trigger('page:load')
diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
index b80a28a33ea..e3788bee813 100644
--- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
+++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
@@ -1,6 +1,6 @@
-%form{ action: '/foo' }
- %input.js-quick-submit{ type: 'text' }
- %textarea.js-quick-submit
+%form.js-quick-submit{ action: '/foo' }
+ %input{ type: 'text' }
+ %textarea
%input{ type: 'submit'} Submit
%button.btn{ type: 'submit' } Submit
diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml
new file mode 100644
index 00000000000..e5850b62659
--- /dev/null
+++ b/spec/javascripts/fixtures/project_title.html.haml
@@ -0,0 +1,7 @@
+%h1.title
+ %a
+ GitLab Org
+ %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"}
+ GitLab Test
+ %input#project_path.project-item-select.js-projects-dropdown.ajax-project-select{type: "hidden", name: "project_path", "data-include-groups" => "false"}
+ %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
new file mode 100644
index 00000000000..84e8d0ba1e4
--- /dev/null
+++ b/spec/javascripts/fixtures/projects.json
@@ -0,0 +1 @@
+[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml
index e867e4de2b9..1701652c61e 100644
--- a/spec/javascripts/fixtures/zen_mode.html.haml
+++ b/spec/javascripts/fixtures/zen_mode.html.haml
@@ -1,9 +1,8 @@
.zennable
- %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' }
.zen-backdrop
- %textarea#note_note.js-gfm-input.markdown-area{placeholder: 'Leave a comment'}
- %a.zen-enter-link{tabindex: '-1'}
+ %textarea#note_note.js-gfm-input.markdown-area
+ %a.js-zen-enter(tabindex="-1" href="#")
%i.fa.fa-expand
- Edit in fullscreen
- %a.zen-leave-link
+ Edit in fullscreen
+ %a.js-zen-leave(tabindex="-1" href="#")
%i.fa.fa-compress
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index 7e67c778861..86ba9dd8e96 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -26,10 +26,10 @@ describe 'reopen/close issue', ->
fixture.load('issues_show.html')
@issue = new Issue()
it 'closes an issue', ->
- $.ajax = (obj) ->
- expect(obj.type).toBe('PUT')
- expect(obj.url).toBe('http://gitlab.com/issues/6/close')
- obj.success saved: true
+ spyOn(jQuery, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PUT')
+ expect(req.url).toBe('http://gitlab.com/issues/6/close')
+ req.success saved: true
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
@@ -44,12 +44,12 @@ describe 'reopen/close issue', ->
expect($('div.status-box-closed')).toBeVisible()
expect($('div.status-box-open')).toBeHidden()
- it 'fails to closes an issue with success:false', ->
+ it 'fails to close an issue with success:false', ->
- $.ajax = (obj) ->
- expect(obj.type).toBe('PUT')
- expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
- obj.success saved: false
+ spyOn(jQuery, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PUT')
+ expect(req.url).toBe('http://goesnowhere.nothing/whereami')
+ req.success saved: false
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
@@ -69,10 +69,10 @@ describe 'reopen/close issue', ->
it 'fails to closes an issue with HTTP error', ->
- $.ajax = (obj) ->
- expect(obj.type).toBe('PUT')
- expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
- obj.error()
+ spyOn(jQuery, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PUT')
+ expect(req.url).toBe('http://goesnowhere.nothing/whereami')
+ req.error()
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
@@ -91,10 +91,10 @@ describe 'reopen/close issue', ->
expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
it 'reopens an issue', ->
- $.ajax = (obj) ->
- expect(obj.type).toBe('PUT')
- expect(obj.url).toBe('http://gitlab.com/issues/6/reopen')
- obj.success saved: true
+ spyOn(jQuery, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PUT')
+ expect(req.url).toBe('http://gitlab.com/issues/6/reopen')
+ req.success saved: true
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee
new file mode 100644
index 00000000000..47c7b7febe3
--- /dev/null
+++ b/spec/javascripts/project_title_spec.js.coffee
@@ -0,0 +1,46 @@
+#= require select2
+#= require api
+#= require project_select
+#= require project
+
+window.gon = {}
+window.gon.api_version = 'v3'
+
+describe 'Project Title', ->
+ fixture.preload('project_title.html')
+ fixture.preload('projects.json')
+
+ beforeEach ->
+ fixture.load('project_title.html')
+ @project = new Project()
+
+ spyOn(@project, 'changeProject').and.callFake (url) ->
+ window.current_project_url = url
+
+ describe 'project list', ->
+ beforeEach =>
+ @projects_data = fixture.load('projects.json')[0]
+
+ spyOn(jQuery, 'ajax').and.callFake (req) =>
+ expect(req.url).toBe('/api/v3/projects.json')
+ d = $.Deferred()
+ d.resolve @projects_data
+ d.promise()
+
+ it 'to show on toggle click', =>
+ $('.js-projects-dropdown-toggle').click()
+
+ expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(true)
+ expect($('.ajax-project-dropdown li').length).toBe(@projects_data.length)
+
+ it 'hide dropdown', ->
+ $("#select2-drop-mask").click()
+
+ expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false)
+
+ it 'change project when clicking item', ->
+ $('.js-projects-dropdown-toggle').click()
+ $('.ajax-project-dropdown li:nth-child(2)').trigger('mouseup')
+
+ expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false)
+ expect(window.current_project_url).toBe('http://localhost:3000/h5bp/html5-boilerplate')
diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee
index 4cb3836755f..b790fce01ed 100644
--- a/spec/javascripts/zen_mode_spec.js.coffee
+++ b/spec/javascripts/zen_mode_spec.js.coffee
@@ -15,14 +15,6 @@ describe 'ZenMode', ->
# Set this manually because we can't actually scroll the window
@zen.scroll_position = 456
- # Ohmmmmmmm
- enterZen = ->
- $('.zen-toggle-comment').prop('checked', true).trigger('change')
-
- # Wh- what was that?!
- exitZen = ->
- $('.zen-toggle-comment').prop('checked', false).trigger('change')
-
describe 'on enter', ->
it 'pauses Mousetrap', ->
spyOn(Mousetrap, 'pause')
@@ -35,16 +27,14 @@ describe 'ZenMode', ->
expect('textarea').not.toHaveAttr('style')
describe 'in use', ->
- beforeEach ->
- enterZen()
+ beforeEach -> enterZen()
it 'exits on Escape', ->
- $(document).trigger(jQuery.Event('keydown', {keyCode: 27}))
- expect($('.zen-toggle-comment').prop('checked')).toBe(false)
+ escapeKeydown()
+ expect($('.zen-backdrop')).not.toHaveClass('fullscreen')
describe 'on exit', ->
- beforeEach ->
- enterZen()
+ beforeEach -> enterZen()
it 'unpauses Mousetrap', ->
spyOn(Mousetrap, 'unpause')
@@ -52,6 +42,10 @@ describe 'ZenMode', ->
expect(Mousetrap.unpause).toHaveBeenCalled()
it 'restores the scroll position', ->
- spyOn(@zen, 'restoreScroll')
+ spyOn(@zen, 'scrollTo')
exitZen()
- expect(@zen.restoreScroll).toHaveBeenCalledWith(456)
+ expect(@zen.scrollTo).toHaveBeenCalled()
+
+enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm
+exitZen = -> $('a.js-zen-leave').click()
+escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27}))
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index 473534ba68a..63a32d9d455 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -21,7 +21,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
let(:reference) { commit.id }
# Let's test a variety of commit SHA sizes just to be paranoid
- [6, 8, 12, 18, 20, 32, 40].each do |size|
+ [7, 8, 12, 18, 20, 32, 40].each do |size|
it "links to a valid reference of #{size} characters" do
doc = reference_filter("See #{reference[0...size]}")
@@ -35,7 +35,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
doc = reference_filter("See #{commit.id}")
expect(doc.text).to eq "See #{commit.short_id}"
- doc = reference_filter("See #{commit.id[0...6]}")
+ doc = reference_filter("See #{commit.id[0...7]}")
expect(doc.text).to eq "See #{commit.short_id}"
end
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index cf314058158..b5b38cf0c8c 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -14,7 +14,7 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'replaces supported emoji' do
doc = filter('<p>:heart:</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png'
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
end
it 'ignores unsupported emoji' do
@@ -25,7 +25,7 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'correctly encodes the URL' do
doc = filter('<p>:+1:</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png'
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
end
it 'matches at the start of a string' do
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
new file mode 100644
index 00000000000..5e23c5c319a
--- /dev/null
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Banzai::Filter::GollumTagsFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:user) { double }
+ let(:project_wiki) { ProjectWiki.new(project, user) }
+
+ describe 'validation' do
+ it 'ensure that a :project_wiki key exists in context' do
+ expect { filter("See [[images/image.jpg]]", {}) }.to raise_error ArgumentError, "Missing context keys for Banzai::Filter::GollumTagsFilter: :project_wiki"
+ end
+ end
+
+ context 'linking internal images' do
+ it 'creates img tag if image exists' do
+ file = Gollum::File.new(project_wiki.wiki)
+ expect(file).to receive(:path).and_return('images/image.jpg')
+ expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file)
+
+ tag = '[[images/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg"
+ end
+
+ it 'does not creates img tag if image does not exist' do
+ expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(nil)
+
+ tag = '[[images/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.css('img').size).to eq 0
+ end
+ end
+
+ context 'linking external images' do
+ it 'creates img tag for valid URL' do
+ tag = '[[http://example.com/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg"
+ end
+
+ it 'does not creates img tag for invalid URL' do
+ tag = '[[http://example.com/image.pdf]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.css('img').size).to eq 0
+ end
+ end
+
+ context 'linking external resources' do
+ it "the created link's text will be equal to the resource's text" do
+ tag = '[[http://example.com]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'http://example.com'
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+
+ it "the created link's text will be link-text" do
+ tag = '[[link-text|http://example.com/pdfs/gollum.pdf]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'link-text'
+ expect(doc.at_css('a')['href']).to eq 'http://example.com/pdfs/gollum.pdf'
+ end
+ end
+
+ context 'linking internal resources' do
+ it "the created link's text will be equal to the resource's text" do
+ tag = '[[wiki-slug]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'wiki-slug'
+ expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ end
+
+ it "the created link's text will be link-text" do
+ tag = '[[link-text|wiki-slug]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'link-text'
+ expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ end
+ end
+
+ context 'table of contents' do
+ it 'replaces [[<em>TOC</em>]] with ToC result' do
+ doc = described_class.call("<p>[[<em>TOC</em>]]</p>", { project_wiki: project_wiki }, { toc: "FOO" })
+
+ expect(doc.to_html).to eq("FOO")
+ end
+
+ it 'handles an empty ToC result' do
+ input = "<p>[[<em>TOC</em>]]</p>"
+ doc = described_class.call(input, project_wiki: project_wiki)
+
+ expect(doc.to_html).to eq ''
+ 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 b46ccc47605..e2d21f53b7e 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -111,7 +111,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
context 'String-based multi-word references in quotes' do
let(:label) { create(:label, name: 'gfm references', project: project) }
- let(:reference) { label.to_reference(:name) }
+ let(:reference) { label.to_reference(format: :name) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
@@ -176,4 +176,29 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(result[:references][:label]).to eq [label]
end
end
+
+ describe 'cross project label references' do
+ let(:another_project) { create(:empty_project, :public) }
+ let(:project_name) { another_project.name_with_namespace }
+ let(:label) { create(:label, project: another_project, color: '#00ff00') }
+ let(:reference) { label.to_reference(project) }
+
+ let!(:result) { reference_filter("See #{reference}") }
+
+ it 'points to referenced project issues page' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(another_project.namespace,
+ another_project,
+ label_name: label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style'))
+ .to match /background-color: #00ff00/
+ end
+
+ it 'contains cross project content' do
+ expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}"
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
new file mode 100644
index 00000000000..ebf3d7489b5
--- /dev/null
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>milestone #{milestone.to_reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline.
+ # Milestone reference behavior in the full Markdown pipeline is tested elsewhere.
+ let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(project.namespace, project, milestone)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("milestone (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(milestone.title)}<\/a>\.\)/)
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("milestone #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}"
+ end
+
+ it 'escapes the title attribute' do
+ milestone.update_attribute(:title, %{"></a>whatever<a title="})
+
+ doc = reference_filter("milestone #{reference}")
+ expect(doc.text).to eq "milestone #{milestone.title}"
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("milestone #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("milestone #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-milestone attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-milestone')
+ expect(link.attr('data-milestone')).to eq milestone.id.to_s
+ end
+
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("milestone #{reference}")
+ expect(result[:references][:milestone]).to eq [milestone]
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index e9bb388e361..9acf6304bcb 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -44,8 +44,78 @@ 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)
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 0b3e5ecbc9f..0e6685f0ffb 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -92,6 +92,14 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
end
+ it 'rebuilds relative URL for a file in the repository root' do
+ relative_link = link('../README.md')
+ doc = filter(relative_link, requested_path: 'doc/some-file.md')
+
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/README.md"
+ end
+
it 'rebuilds relative URL for a file in the repo with an anchor' do
doc = filter(link('README.md#section'))
expect(doc.at_css('a')['href']).
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 760d60a4190..27ce312b11c 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -75,6 +75,11 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
+ it 'allows `abbr` elements' do
+ exp = act = %q{<abbr title="HyperText Markup Language">HTML</abbr>}
+ expect(filter(act).to_html).to eq exp
+ end
+
it 'removes `rel` attribute from `a` elements' do
act = %q{<a href="#" rel="nofollow">Link</a>}
exp = %q{<a href="#">Link</a>}
@@ -144,20 +149,54 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
output: '<a href="java"></a>'
},
+ 'protocol-based JS injection: invalid URL char' => {
+ input: '<img src=java\script:alert("XSS")>',
+ output: '<img>'
+ },
+
'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
output: '<a href="">foo</a>'
},
+
+ 'protocol whitespace' => {
+ input: '<a href=" http://example.com/"></a>',
+ output: '<a href="http://example.com/"></a>'
+ }
}
protocols.each do |name, data|
- it "handles #{name}" do
+ it "disallows #{name}" do
doc = filter(data[:input])
expect(doc.to_html).to eq data[:output]
end
end
+ it 'disallows data links' do
+ input = '<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">XSS</a>'
+ output = filter(input)
+
+ expect(output.to_html).to eq '<a>XSS</a>'
+ end
+
+ it 'disallows vbscript links' do
+ input = '<a href="vbscript:alert(document.domain)">XSS</a>'
+ output = filter(input)
+
+ expect(output.to_html).to eq '<a>XSS</a>'
+ end
+
+ it 'disallows invalid URIs' do
+ expect(Addressable::URI).to receive(:parse).with('foo://example.com').
+ and_raise(Addressable::URI::InvalidURIError)
+
+ input = '<a href="foo://example.com">Foo</a>'
+ output = filter(input)
+
+ expect(output.to_html).to eq '<a>Foo</a>'
+ end
+
it 'allows non-standard anchor schemes' do
exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
act = filter(exp)
@@ -172,26 +211,4 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
expect(act.to_html).to eq exp
end
end
-
- context 'when inline_sanitization is true' do
- it 'uses a stricter whitelist' do
- doc = filter('<h1>Description</h1>', inline_sanitization: true)
- expect(doc.to_html.strip).to eq 'Description'
- end
-
- %w(pre code img ol ul li).each do |elem|
- it "removes '#{elem}' elements" do
- act = "<#{elem}>Description</#{elem}>"
- expect(filter(act, inline_sanitization: true).to_html.strip).
- to eq 'Description'
- end
- end
-
- %w(b i strong em a ins del sup sub p).each do |elem|
- it "still allows '#{elem}' elements" do
- exp = act = "<#{elem}>Description</#{elem}>"
- expect(filter(act, inline_sanitization: true).to_html).to eq exp
- end
- end
- end
end
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
index f2e3a44478d..569cbc885c7 100644
--- a/spec/lib/banzai/filter/task_list_filter_spec.rb
+++ b/spec/lib/banzai/filter/task_list_filter_spec.rb
@@ -7,4 +7,10 @@ describe Banzai::Filter::TaskListFilter, lib: true do
exp = act = %(<ul><li>Item</li></ul>)
expect(filter(act).to_html).to eq exp
end
+
+ it 'applies `task-list` to single-item task lists' do
+ act = filter('<ul><li>[ ] Task 1</li></ul>')
+
+ expect(act.to_html).to start_with '<ul class="task-list">'
+ end
end
diff --git a/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb b/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb
new file mode 100644
index 00000000000..fe70eada7eb
--- /dev/null
+++ b/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+describe Banzai::Filter::YamlFrontMatterFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'allows for `encoding:` before the frontmatter' do
+ content = <<-MD.strip_heredoc
+ # encoding: UTF-8
+ ---
+ foo: foo
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ expect(output).not_to match 'encoding'
+ end
+
+ it 'converts YAML frontmatter to a fenced code block' do
+ content = <<-MD.strip_heredoc
+ ---
+ bar: :bar_symbol
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include '---'
+ expect(output).to include "```yaml\nbar: :bar_symbol\n```"
+ end
+ end
+
+ context 'on content without frontmatter' do
+ it 'returns the content unmodified' do
+ content = <<-MD.strip_heredoc
+ # This is some Markdown
+
+ It has no YAML frontmatter to parse.
+ MD
+
+ expect(filter(content)).to eq content
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter_array_spec.rb b/spec/lib/banzai/filter_array_spec.rb
new file mode 100644
index 00000000000..ea84005e7f8
--- /dev/null
+++ b/spec/lib/banzai/filter_array_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Banzai::FilterArray do
+ describe '#insert_after' do
+ it 'inserts an element after a provided element' do
+ filters = described_class.new(%w(a b c))
+
+ filters.insert_after('b', '1')
+
+ expect(filters).to eq %w(a b 1 c)
+ end
+
+ it 'inserts an element at the end when the provided element does not exist' do
+ filters = described_class.new(%w(a b c))
+
+ filters.insert_after('d', '1')
+
+ expect(filters).to eq %w(a b c 1)
+ end
+ end
+
+ describe '#insert_before' do
+ it 'inserts an element before a provided element' do
+ filters = described_class.new(%w(a b c))
+
+ filters.insert_before('b', '1')
+
+ expect(filters).to eq %w(a 1 b c)
+ end
+
+ it 'inserts an element at the beginning when the provided element does not exist' do
+ filters = described_class.new(%w(a b c))
+
+ filters.insert_before('d', '1')
+
+ expect(filters).to eq %w(1 a b c)
+ end
+ end
+end
diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
new file mode 100644
index 00000000000..76f42071810
--- /dev/null
+++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+describe Banzai::Pipeline::DescriptionPipeline do
+ def parse(html)
+ # When we pass HTML to Redcarpet, it gets wrapped in `p` tags...
+ # ...except when we pass it pre-wrapped text. Rabble rabble.
+ unwrap = !html.start_with?('<p>')
+
+ output = described_class.to_html(html, project: spy)
+
+ output.gsub!(%r{\A<p>(.*)</p>(.*)\z}, '\1\2') if unwrap
+
+ output
+ end
+
+ it 'uses a limited whitelist' do
+ doc = parse('# Description')
+
+ expect(doc.strip).to eq 'Description'
+ end
+
+ %w(pre code img ol ul li).each do |elem|
+ it "removes '#{elem}' elements" do
+ act = "<#{elem}>Description</#{elem}>"
+
+ expect(parse(act).strip).to eq 'Description'
+ end
+ end
+
+ %w(b i strong em a ins del sup sub p).each do |elem|
+ it "still allows '#{elem}' elements" do
+ exp = act = "<#{elem}>Description</#{elem}>"
+
+ expect(parse(act).strip).to eq exp
+ end
+ end
+end
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
new file mode 100644
index 00000000000..3e25406e498
--- /dev/null
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+describe Banzai::Pipeline::WikiPipeline do
+ describe 'TableOfContents' do
+ it 'replaces the tag with the TableOfContentsFilter result' do
+ markdown = <<-MD.strip_heredoc
+ [[_TOC_]]
+
+ ## Header
+
+ Foo
+ MD
+
+ result = described_class.call(markdown, project: spy, project_wiki: double)
+
+ aggregate_failures do
+ expect(result[:output].text).not_to include '[['
+ expect(result[:output].text).not_to include 'TOC'
+ expect(result[:output].to_html).to include(result[:toc])
+ end
+ end
+
+ it 'is case-sensitive' do
+ markdown = <<-MD.strip_heredoc
+ [[_toc_]]
+
+ # Header 1
+
+ Foo
+ MD
+
+ output = described_class.to_html(markdown, project: spy, project_wiki: double)
+
+ expect(output).to include('[[<em>toc</em>]]')
+ end
+
+ it 'handles an empty pipeline result' do
+ # No Markdown headers in this doc, so `result[:toc]` will be empty
+ markdown = <<-MD.strip_heredoc
+ [[_TOC_]]
+
+ Foo
+ MD
+
+ output = described_class.to_html(markdown, project: spy, project_wiki: double)
+
+ aggregate_failures do
+ expect(output).not_to include('<ul>')
+ expect(output).not_to include('[[<em>TOC</em>]]')
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb
new file mode 100644
index 00000000000..27da2a7439c
--- /dev/null
+++ b/spec/lib/banzai/querying_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Banzai::Querying do
+ describe '.css' do
+ it 'optimizes queries for elements with classes' do
+ document = double(:document)
+
+ expect(document).to receive(:xpath).with(/^descendant::a/)
+
+ described_class.css(document, 'a.gfm')
+ end
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index d15100fc6d8..fab6412d29f 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -336,7 +336,7 @@ module Ci
describe "Caches" do
it "returns cache when defined globally" do
config = YAML.dump({
- cache: { paths: ["logs/", "binaries/"], untracked: true },
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
rspec: {
script: "rspec"
}
@@ -348,13 +348,14 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
+ key: 'key',
)
end
it "returns cache when defined in a job" do
config = YAML.dump({
rspec: {
- cache: { paths: ["logs/", "binaries/"], untracked: true },
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
script: "rspec"
}
})
@@ -365,15 +366,16 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
+ key: 'key',
)
end
it "overwrite cache when defined for a job and globally" do
config = YAML.dump({
- cache: { paths: ["logs/", "binaries/"], untracked: true },
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
rspec: {
script: "rspec",
- cache: { paths: ["test/"], untracked: false },
+ cache: { paths: ["test/"], untracked: false, key: 'local' },
}
})
@@ -383,6 +385,7 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["test/"],
untracked: false,
+ key: 'local',
)
end
end
@@ -394,7 +397,7 @@ module Ci
services: ["mysql"],
before_script: ["pwd"],
rspec: {
- artifacts: { paths: ["logs/", "binaries/"], untracked: true },
+ artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" },
script: "rspec"
}
})
@@ -414,6 +417,7 @@ module Ci
image: "ruby:2.1",
services: ["mysql"],
artifacts: {
+ name: "custom_name",
paths: ["logs/", "binaries/"],
untracked: true
}
@@ -424,6 +428,112 @@ module Ci
end
end
+ describe "Dependencies" do
+ let(:config) do
+ {
+ build1: { stage: 'build', script: 'test' },
+ build2: { stage: 'build', script: 'test' },
+ test1: { stage: 'test', script: 'test', dependencies: dependencies },
+ test2: { stage: 'test', script: 'test' },
+ deploy: { stage: 'test', script: 'test' }
+ }
+ end
+
+ subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ context 'no dependencies' do
+ let(:dependencies) { }
+
+ it { expect { subject }.to_not raise_error }
+ end
+
+ context 'dependencies to builds' do
+ let(:dependencies) { [:build1, :build2] }
+
+ it { expect { subject }.to_not raise_error }
+ end
+
+ context 'undefined dependency' do
+ 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] }
+
+ it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
+ end
+ end
+
+ describe "Hidden jobs" do
+ let(:config) do
+ YAML.dump({
+ '.hidden_job' => { script: 'test' },
+ 'normal_job' => { script: 'test' }
+ })
+ end
+
+ let(:config_processor) { GitlabCiYamlProcessor.new(config) }
+
+ subject { config_processor.builds_for_stage_and_ref("test", "master") }
+
+ it "doesn't create jobs that starts with dot" do
+ expect(subject.size).to eq(1)
+ expect(subject.first).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :normal_job,
+ only: nil,
+ commands: "\ntest",
+ tag_list: [],
+ options: {},
+ when: "on_success",
+ allow_failure: false
+ })
+ end
+ end
+
+ describe "YAML Alias/Anchor" do
+ it "is correctly supported for jobs" do
+ config = <<EOT
+job1: &JOBTMPL
+ script: execute-script-for-job
+
+job2: *JOBTMPL
+EOT
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(2)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :job1,
+ only: nil,
+ commands: "\nexecute-script-for-job",
+ tag_list: [],
+ options: {},
+ when: "on_success",
+ allow_failure: false
+ })
+ expect(config_processor.builds_for_stage_and_ref("test", "master").second).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :job2,
+ only: nil,
+ commands: "\nexecute-script-for-job",
+ tag_list: [],
+ options: {},
+ when: "on_success",
+ allow_failure: false
+ })
+ end
+ end
+
describe "Error handling" do
it "fails to parse YAML" do
expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
@@ -587,6 +697,13 @@ module Ci
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
end
+ it "returns errors if job artifacts:name is not an a string" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
+ end
+
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
@@ -615,6 +732,20 @@ module Ci
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths parameter should be an array of strings")
end
+ it "returns errors if cache:key is not a string" do
+ config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key parameter should be a string")
+ end
+
+ it "returns errors if job cache:key is not an a string" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:key parameter should be a string")
+ end
+
it "returns errors if job cache:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } })
expect do
@@ -628,6 +759,13 @@ module Ci
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
end
+
+ it "returns errors if job dependencies is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings")
+ end
end
end
end
diff --git a/spec/lib/ci/status_spec.rb b/spec/lib/ci/status_spec.rb
new file mode 100644
index 00000000000..47f3df6e3ce
--- /dev/null
+++ b/spec/lib/ci/status_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Ci::Status do
+ describe '.get_status' do
+ subject { described_class.get_status(statuses) }
+
+ shared_examples 'build status summary' do
+ context 'all successful' do
+ let(:statuses) { Array.new(2) { create(type, status: :success) } }
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'at least one failed' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :failed)]
+ end
+
+ it { is_expected.to eq 'failed' }
+ end
+
+ context 'at least one running' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :running)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'at least one pending' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :pending)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'success and failed but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :failed, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'one failed but allowed to fail' do
+ let(:statuses) { [create(type, status: :failed, allow_failure: true)] }
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'success and canceled' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :canceled)]
+ end
+ it { is_expected.to eq 'failed' }
+ end
+
+ context 'all canceled' do
+ let(:statuses) do
+ [create(type, status: :canceled), create(type, status: :canceled)]
+ end
+ it { is_expected.to eq 'canceled' }
+ end
+
+ context 'success and canceled but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :canceled, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'one finished and second running but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :running, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+ end
+
+ context 'ci build statuses' do
+ let(:type) { :ci_build }
+ it_behaves_like 'build status summary'
+ end
+
+ context 'generic commit statuses' do
+ let(:type) { :generic_commit_status }
+ it_behaves_like 'build status summary'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb
new file mode 100644
index 00000000000..9858935180a
--- /dev/null
+++ b/spec/lib/gitlab/akismet_helper_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::AkismetHelper, type: :helper do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
+ current_application_settings.akismet_enabled = true
+ current_application_settings.akismet_api_key = '12345'
+ end
+
+ describe '#check_for_spam?' do
+ it 'returns true for non-member' do
+ expect(helper.check_for_spam?(project, user)).to eq(true)
+ end
+
+ it 'returns false for member' do
+ project.team << [user, :guest]
+ expect(helper.check_for_spam?(project, user)).to eq(false)
+ end
+ end
+
+ describe '#is_spam?' do
+ it 'returns true for spam' do
+ environment = {
+ 'REMOTE_ADDR' => '127.0.0.1',
+ 'HTTP_USER_AGENT' => 'Test User Agent'
+ }
+
+ allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true])
+ expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 6beb21c6d2b..736bf787208 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -42,22 +42,6 @@ module Gitlab
end
end
- context "with project in context" do
-
- let(:context) { { project: create(:project) } }
-
- it "should filter converted input via HTML pipeline and return result" do
- filtered_html = '<b>ASCII</b>'
-
- allow(Asciidoctor).to receive(:convert).and_return(html)
- expect(Banzai).to receive(:render)
- .with(html, context.merge(pipeline: :asciidoc))
- .and_return(filtered_html)
-
- expect( render('foo', context) ).to eql filtered_html
- end
- end
-
def render(*args)
described_class.render(*args)
end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
new file mode 100644
index 00000000000..c413132abe5
--- /dev/null
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Gitlab::BitbucketImport::Importer, lib: true do
+ before do
+ Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
+ end
+
+ let(:statuses) do
+ [
+ "open",
+ "resolved",
+ "on hold",
+ "invalid",
+ "duplicate",
+ "wontfix",
+ "closed" # undocumented status
+ ]
+ end
+ let(:sample_issues_statuses) do
+ issues = []
+
+ statuses.map.with_index do |status, index|
+ issues << {
+ local_id: index,
+ status: status,
+ title: "Issue #{index}",
+ content: "Some content to issue #{index}"
+ }
+ end
+
+ issues
+ end
+
+ let(:project_identifier) { 'namespace/repo' }
+ let(:data) do
+ {
+ bb_session: {
+ bitbucket_access_token: "123456",
+ bitbucket_access_token_secret: "secret"
+ }
+ }
+ end
+ let(:project) do
+ create(
+ :project,
+ import_source: project_identifier,
+ import_data: ProjectImportData.new(data: data)
+ )
+ end
+ let(:importer) { Gitlab::BitbucketImport::Importer.new(project) }
+ let(:issues_statuses_sample_data) do
+ {
+ count: sample_issues_statuses.count,
+ issues: sample_issues_statuses
+ }
+ end
+
+ context 'issues statuses' do
+ before do
+ stub_request(
+ :get,
+ "https://bitbucket.org/api/1.0/repositories/#{project_identifier}"
+ ).to_return(status: 200, body: { has_issues: true }.to_json)
+
+ stub_request(
+ :get,
+ "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0"
+ ).to_return(status: 200, body: issues_statuses_sample_data.to_json)
+
+ sample_issues_statuses.each_with_index do |issue, index|
+ stub_request(
+ :get,
+ "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments"
+ ).to_return(
+ status: 200,
+ body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json
+ )
+ end
+ end
+
+ it 'map statuses to open or closed' do
+ importer.execute
+
+ expect(project.issues.where(state: "closed").size).to eq(5)
+ expect(project.issues.where(state: "opened").size).to eq(2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/blame_spec.rb b/spec/lib/gitlab/blame_spec.rb
new file mode 100644
index 00000000000..89245761b6f
--- /dev/null
+++ b/spec/lib/gitlab/blame_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::Blame, lib: true do
+ let(:project) { create(:project) }
+ let(:path) { 'files/ruby/popen.rb' }
+ let(:commit) { project.commit('master') }
+ let(:blob) { project.repository.blob_at(commit.id, path) }
+
+ describe "#groups" do
+ let(:subject) { described_class.new(blob, commit).groups(highlight: false) }
+
+ it 'groups lines properly' do
+ expect(subject.count).to eq(18)
+ expect(subject[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e')
+ expect(subject[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""])
+
+ expect(subject[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e')
+ expect(subject[1][:lines]).to eq(["module Popen", " extend self"])
+
+ expect(subject[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e')
+ expect(subject[-1][:lines]).to eq([" end", "end"])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb
index 839b30f1ff4..38be9448794 100644
--- a/spec/lib/gitlab/build_data_builder_spec.rb
+++ b/spec/lib/gitlab/build_data_builder_spec.rb
@@ -14,6 +14,7 @@ describe 'Gitlab::BuildDataBuilder' do
it { expect(data[:tag]).to eq(build.tag) }
it { expect(data[:build_id]).to eq(build.id) }
it { expect(data[:build_status]).to eq(build.status) }
+ it { expect(data[:build_allow_failure]).to eq(false) }
it { expect(data[:project_id]).to eq(build.project.id) }
it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
new file mode 100644
index 00000000000..acca0b08bab
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
+ let(:entries) do
+ { 'path/' => {},
+ 'path/dir_1/' => {},
+ 'path/dir_1/file_1' => { size: 10 },
+ 'path/dir_1/file_b' => { size: 10 },
+ 'path/dir_1/subdir/' => {},
+ 'path/dir_1/subdir/subfile' => { size: 10 },
+ 'path/second_dir' => {},
+ 'path/second_dir/dir_3/file_2' => { size: 10 },
+ 'path/second_dir/dir_3/file_3'=> { size: 10 },
+ 'another_directory/'=> {},
+ 'another_file' => {},
+ '/file/with/absolute_path' => {} }
+ end
+
+ def path(example)
+ entry(example.metadata[:path])
+ end
+
+ def entry(path)
+ described_class.new(path, entries)
+ end
+
+ describe '/file/with/absolute_path', path: '/file/with/absolute_path' do
+ subject { |example| path(example) }
+
+ it { is_expected.to be_file }
+ it { is_expected.to have_parent }
+
+ describe '#basename' do
+ subject { |example| path(example).basename }
+ it { is_expected.to eq 'absolute_path' }
+ end
+ end
+
+ describe 'path/dir_1/', path: 'path/dir_1/' do
+ subject { |example| path(example) }
+ it { is_expected.to have_parent }
+ it { is_expected.to be_directory }
+
+ describe '#basename' do
+ subject { |example| path(example).basename }
+ it { is_expected.to eq 'dir_1/' }
+ end
+
+ describe '#name' do
+ subject { |example| path(example).name }
+ it { is_expected.to eq 'dir_1' }
+ end
+
+ describe '#parent' do
+ subject { |example| path(example).parent }
+ it { is_expected.to eq entry('path/') }
+ end
+
+ describe '#children' do
+ subject { |example| path(example).children }
+
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b'),
+ entry('path/dir_1/subdir/')
+ end
+ end
+
+ describe '#files' do
+ subject { |example| path(example).files }
+
+ it { is_expected.to all(be_file) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b')
+ end
+ end
+
+ describe '#directories' do
+ context 'without options' do
+ subject { |example| path(example).directories }
+
+ it { is_expected.to all(be_directory) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it { is_expected.to contain_exactly entry('path/dir_1/subdir/') }
+ end
+
+ context 'with option parent: true' do
+ subject { |example| path(example).directories(parent: true) }
+
+ it { is_expected.to all(be_directory) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/subdir/'),
+ entry('path/')
+ end
+ end
+
+ describe '#nodes' do
+ subject { |example| path(example).nodes }
+ it { is_expected.to eq 2 }
+ end
+
+ describe '#exists?' do
+ subject { |example| path(example).exists? }
+ it { is_expected.to be true }
+ end
+
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be false }
+ end
+
+ describe '#total_size' do
+ subject { |example| path(example).total_size }
+ it { is_expected.to eq(30) }
+ end
+ end
+ end
+
+ describe 'empty path', path: '' do
+ subject { |example| path(example) }
+ it { is_expected.to_not have_parent }
+
+ describe '#children' do
+ subject { |example| path(example).children }
+ it { expect(subject.count).to eq 3 }
+ end
+
+ end
+
+ describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do
+ describe '#nodes' do
+ subject { |example| path(example).nodes }
+ it { is_expected.to eq 4 }
+ end
+ end
+
+ describe 'non-existent/', path: 'non-existent/' do
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be true }
+ end
+
+ describe '#exists?' do
+ subject { |example| path(example).exists? }
+ it { is_expected.to be false }
+ end
+ end
+
+ describe 'another_directory/', path: 'another_directory/' do
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be true }
+ end
+ end
+
+ describe '#metadata' do
+ let(:entries) do
+ { 'path/' => { name: '/path/' },
+ 'path/file1' => { name: '/path/file1' },
+ 'path/file2' => { name: '/path/file2' } }
+ end
+
+ subject do
+ described_class.new('path/file1', entries).metadata[:name]
+ end
+
+ it { is_expected.to eq '/path/file1' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
new file mode 100644
index 00000000000..eea01f91879
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Artifacts::Metadata do
+ def metadata(path = '', **opts)
+ described_class.new(metadata_file_path, path, **opts)
+ end
+
+ let(:metadata_file_path) do
+ Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+ end
+
+ context 'metadata file exists' do
+ describe '#find_entries! empty string' do
+ subject { metadata('').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).to contain_exactly 'ci_artifacts.txt',
+ 'other_artifacts_0.1.2/',
+ 'rails_sample.jpg',
+ 'tests_encoding/'
+ end
+
+ it 'matches metadata for every path' do
+ expect(subject.keys.count).to eq 4
+ end
+
+ it 'return Hashes for each metadata' do
+ expect(subject.values).to all(be_kind_of(Hash))
+ end
+ end
+
+ describe '#find_entries! other_artifacts_0.1.2/' do
+ subject { metadata('other_artifacts_0.1.2/').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).
+ to contain_exactly 'other_artifacts_0.1.2/',
+ 'other_artifacts_0.1.2/doc_sample.txt',
+ 'other_artifacts_0.1.2/another-subdirectory/'
+ end
+ end
+
+ describe '#find_entries! other_artifacts_0.1.2/another-subdirectory/' do
+ subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).
+ to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/',
+ 'other_artifacts_0.1.2/another-subdirectory/empty_directory/',
+ 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif'
+ end
+ end
+
+ describe '#find_entries! recursively for other_artifacts_0.1.2/' do
+ subject { metadata('other_artifacts_0.1.2/', recursive: true).find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).
+ to contain_exactly 'other_artifacts_0.1.2/',
+ 'other_artifacts_0.1.2/doc_sample.txt',
+ 'other_artifacts_0.1.2/another-subdirectory/',
+ 'other_artifacts_0.1.2/another-subdirectory/empty_directory/',
+ 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif'
+ end
+ end
+
+ describe '#to_entry' do
+ subject { metadata('').to_entry }
+ it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) }
+ end
+
+ describe '#full_version' do
+ subject { metadata('').full_version }
+ it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' }
+ end
+
+ describe '#version' do
+ subject { metadata('').version }
+ it { is_expected.to eq '0.0.1' }
+ end
+
+ describe '#errors' do
+ subject { metadata('').errors }
+ it { is_expected.to eq({}) }
+ end
+ end
+
+ context 'metadata file does not exist' do
+ let(:metadata_file_path) { '' }
+
+ describe '#find_entries!' do
+ it 'raises error' do
+ expect { metadata.find_entries! }.to raise_error(Errno::ENOENT)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 99288da1e43..844fd79c991 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
@@ -135,6 +136,17 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
message = "resolve #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
+
+ context 'with an external issue tracker reference' do
+ it 'extracts the referenced issue' do
+ jira_project = create(:jira_project, name: 'JIRA_EXT1')
+ jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project)
+ closing_issue_extractor = described_class.new jira_project
+ message = "Resolve #{jira_issue.to_reference}"
+
+ expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue])
+ end
+ end
end
context "with a cross-project reference" do
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 8461e8ce50d..d0a447753b7 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -1,5 +1,9 @@
require 'spec_helper'
+class MigrationTest
+ include Gitlab::Database
+end
+
describe Gitlab::Database, lib: true do
# These are just simple smoke tests to check if the methods work (regardless
# of what they may return).
@@ -14,4 +18,52 @@ describe Gitlab::Database, lib: true do
it { is_expected.to satisfy { |val| val == true || val == false } }
end
+
+ describe '.version' do
+ context "on mysql" do
+ it "extracts the version number" do
+ allow(described_class).to receive(:database_version).
+ and_return("5.7.12-standard")
+
+ expect(described_class.version).to eq '5.7.12-standard'
+ end
+ end
+
+ context "on postgresql" do
+ it "extracts the version number" do
+ allow(described_class).to receive(:database_version).
+ and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0")
+
+ expect(described_class.version).to eq '9.4.4'
+ end
+ end
+ end
+
+ describe '#true_value' do
+ it 'returns correct value for PostgreSQL' do
+ expect(described_class).to receive(:postgresql?).and_return(true)
+
+ expect(MigrationTest.new.true_value).to eq "'t'"
+ end
+
+ it 'returns correct value for MySQL' do
+ expect(described_class).to receive(:postgresql?).and_return(false)
+
+ expect(MigrationTest.new.true_value).to eq 1
+ end
+ end
+
+ describe '#false_value' do
+ it 'returns correct value for PostgreSQL' do
+ expect(described_class).to receive(:postgresql?).and_return(true)
+
+ expect(MigrationTest.new.false_value).to eq "'f'"
+ end
+
+ it 'returns correct value for MySQL' do
+ expect(described_class).to receive(:postgresql?).and_return(false)
+
+ expect(MigrationTest.new.false_value).to eq 0
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index c7cdf8691d6..a0cbef6e6a4 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do
let(:project) { create(:project) }
let(:commit) { project.commit(sample_commit.id) }
let(:diff) { commit.diffs.first }
- let(:diff_file) { Gitlab::Diff::File.new(diff) }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) }
describe :diff_lines do
let(:diff_lines) { diff_file.diff_lines }
@@ -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/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
new file mode 100644
index 00000000000..d19bf4ac84b
--- /dev/null
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::Highlight, lib: true do
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:diff) { commit.diffs.first }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) }
+
+ describe '#highlight' do
+ context "with a diff file" do
+ let(:subject) { Gitlab::Diff::Highlight.new(diff_file).highlight }
+
+ it 'should return Gitlab::Diff::Line elements' do
+ expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
+ end
+
+ it 'should not modify "match" lines' do
+ expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen')
+ expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen')
+ end
+
+ it 'highlights and marks unchanged lines' do
+ code = %Q{ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
+
+ expect(subject[2].text).to eq(code)
+ end
+
+ it 'highlights and marks removed lines' do
+ code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>\n}
+
+ expect(subject[4].text).to eq(code)
+ end
+
+ it 'highlights and marks added lines' do
+ code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>\n}
+
+ expect(subject[5].text).to eq(code)
+ end
+ end
+
+ context "with diff lines" do
+ let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight }
+
+ it 'should return Gitlab::Diff::Line elements' do
+ expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
+ end
+
+ it 'should not modify "match" lines' do
+ expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen')
+ expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen')
+ end
+
+ it 'marks unchanged lines' do
+ code = %Q{ def popen(cmd, path=nil)}
+
+ expect(subject[2].text).to eq(code)
+ expect(subject[2].text).not_to be_html_safe
+ end
+
+ it 'marks removed lines' do
+ code = %Q{- raise "System commands must be given as an array of strings"}
+
+ expect(subject[4].text).to eq(code)
+ expect(subject[4].text).not_to be_html_safe
+ end
+
+ it 'marks added lines' do
+ code = %Q{+ raise <span class='idiff left right'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
+
+ expect(subject[5].text).to eq(code)
+ expect(subject[5].text).to be_html_safe
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
new file mode 100644
index 00000000000..ea5c31011f0
--- /dev/null
+++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::InlineDiffMarker, lib: true do
+ describe '#inline_diffs' do
+
+ context "when the rich text is html safe" do
+ let(:raw) { "abc 'def'" }
+ let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&#39;def&#39;</span>}.html_safe }
+ let(:inline_diffs) { [2..5] }
+ let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) }
+
+ it 'marks the inline diffs' do
+ expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'>&#39;d</span>ef&#39;</span>})
+ expect(subject).to be_html_safe
+ end
+ end
+
+ context "when the text text is not html safe" do
+ let(:raw) { "abc 'def'" }
+ let(:inline_diffs) { [2..5] }
+ let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) }
+
+ it 'marks the inline diffs' do
+ expect(subject).to eq(%{ab<span class='idiff left right'>c &#39;d</span>ef&#39;})
+ expect(subject).to be_html_safe
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb
new file mode 100644
index 00000000000..95a993d26cf
--- /dev/null
+++ b/spec/lib/gitlab/diff/inline_diff_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::InlineDiff, lib: true do
+ describe '.for_lines' do
+ let(:diff) do
+ <<eos
+ class Test
+- def initialize(test = true)
++ def initialize(test = false)
+ @test = test
+ end
+ end
+eos
+ end
+
+ let(:subject) { described_class.for_lines(diff.lines) }
+
+ it 'finds all inline diffs' do
+ expect(subject[0]).to be_nil
+ expect(subject[1]).to eq([25..27])
+ expect(subject[2]).to eq([25..28])
+ expect(subject[3]).to be_nil
+ expect(subject[4]).to be_nil
+ expect(subject[5]).to be_nil
+ end
+ end
+
+ describe "#inline_diffs" do
+ let(:old_line) { "XXX def initialize(test = true)" }
+ let(:new_line) { "YYY def initialize(test = false)" }
+ let(:subject) { described_class.new(old_line, new_line, offset: 3).inline_diffs }
+
+ it "finds the inline diff" do
+ old_diffs, new_diffs = subject
+
+ expect(old_diffs).to eq([26..28])
+ expect(new_diffs).to eq([26..29])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb
new file mode 100644
index 00000000000..1c5bbc47120
--- /dev/null
+++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::ParallelDiff, lib: true do
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:diffs) { commit.diffs }
+ let(:diff) { diffs.first }
+ let(:diff_refs) { [commit.parent, commit] }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) }
+ subject { described_class.new(diff_file) }
+
+ let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") }
+
+ describe '#parallelize' do
+ it 'should return an array of arrays containing the parsed diff' do
+ expect(subject.parallelize).to match_array(parallel_diff_result_array)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index ba577bd28e5..cdff063a9ed 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -47,7 +47,7 @@ eos
end
before do
- @lines = parser.parse(diff.lines)
+ @lines = parser.parse(diff.lines).to_a
end
it { expect(@lines.size).to eq(30) }
@@ -86,8 +86,13 @@ eos
it { expect(line.type).to eq(nil) }
it { expect(line.old_pos).to eq(24) }
it { expect(line.new_pos).to eq(31) }
- it { expect(line.text).to eq(' @cmd_output &lt;&lt; stderr.read') }
+ it { expect(line.text).to eq(' @cmd_output << stderr.read') }
end
end
end
+
+ context 'when lines is empty' do
+ it { expect(parser.parse([])).to eq([]) }
+ it { expect(parser.parse(nil)).to eq([]) }
+ end
end
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index 56ae2a8d121..b2d7a799810 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -72,7 +72,7 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#compare_timeout' do
subject { message.compare_timeout }
- it { is_expected.to eq compare.timeout }
+ it { is_expected.to eq compare.diffs.overflow? }
end
describe '#reverse_compare?' do
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index b535413bbd4..abe179cd4af 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::Email::Receiver, lib: true do
context "when the email was auto generated" do
let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
let!(:email_raw) { fixture_file("emails/auto_reply.eml") }
-
+
it "raises an AutoGeneratedEmailError" do
expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError)
end
@@ -90,7 +90,7 @@ describe Gitlab::Email::Receiver, lib: true do
context "when the reply is blank" do
let!(:email_raw) { fixture_file("emails/no_content_reply.eml") }
-
+
it "raises an EmptyEmailError" do
expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError)
end
@@ -107,13 +107,16 @@ describe Gitlab::Email::Receiver, lib: true do
end
context "when everything is fine" do
+ let(:markdown) { "![image](uploads/image.png)" }
+
before do
allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return(
[
{
url: "uploads/image.png",
is_image: true,
- alt: "image"
+ alt: "image",
+ markdown: markdown
}
]
)
@@ -132,7 +135,7 @@ describe Gitlab::Email::Receiver, lib: true do
note = noteable.notes.last
- expect(note.note).to include("![image](uploads/image.png)")
+ expect(note.note).to include(markdown)
end
end
end
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
new file mode 100644
index 00000000000..fbdb7ea34ac
--- /dev/null
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::ExclusiveLease do
+ it 'cannot obtain twice before the lease has expired' do
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
+ expect(lease.try_obtain).to eq(true)
+ expect(lease.try_obtain).to eq(false)
+ end
+
+ it 'can obtain after the lease has expired' do
+ timeout = 1
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
+ lease.try_obtain # start the lease
+ sleep(2 * timeout) # lease should have expired now
+ expect(lease.try_obtain).to eq(true)
+ end
+
+ def unique_key
+ SecureRandom.hex(10)
+ end
+end
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
new file mode 100644
index 00000000000..a324a82e69f
--- /dev/null
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::CommentFormatter, lib: true do
+ let(:project) { create(:project) }
+ let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
+ let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
+ let(:base_data) do
+ {
+ body: "I'm having a problem with this.",
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ end
+
+ subject(:comment) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when do not reference a portion of the diff' do
+ let(:raw_data) { OpenStruct.new(base_data) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ note: "*Created by: octocat*\n\nI'm having a problem with this.",
+ commit_id: nil,
+ line_code: nil,
+ author_id: project.creator_id,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(comment.attributes).to eq(expected)
+ end
+ end
+
+ context 'when on a portion of the diff' do
+ let(:diff_data) do
+ {
+ body: 'Great stuff',
+ commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
+ diff_hunk: '@@ -16,33 +16,40 @@ public class Connection : IConnection...',
+ path: 'file1.txt',
+ position: 1
+ }
+ end
+
+ let(:raw_data) { OpenStruct.new(base_data.merge(diff_data)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ note: "*Created by: octocat*\n\nGreat stuff",
+ commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
+ line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_0_1',
+ author_id: project.creator_id,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(comment.attributes).to eq(expected)
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+
+ it 'returns project#creator_id as author_id when is not a GitLab user' do
+ expect(comment.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id as author_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
new file mode 100644
index 00000000000..fd05428b322
--- /dev/null
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -0,0 +1,139 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::IssueFormatter, lib: true do
+ let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+ let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+
+ let(:base_data) do
+ {
+ number: 1347,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil
+ }
+ end
+
+ subject(:issue) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when issue is open' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ title: 'Found a bug',
+ description: "*Created by: octocat*\n\nI'm having a problem with this.",
+ state: 'opened',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(issue.attributes).to eq(expected)
+ end
+ end
+
+ context 'when issue is closed' do
+ let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ title: 'Found a bug',
+ description: "*Created by: octocat*\n\nI'm having a problem with this.",
+ state: 'closed',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: closed_at
+ }
+
+ expect(issue.attributes).to eq(expected)
+ end
+ end
+
+ context 'when it is assigned to someone' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) }
+
+ it 'returns nil as assignee_id when is not a GitLab user' do
+ expect(issue.attributes.fetch(:assignee_id)).to be_nil
+ end
+
+ it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+
+ it 'returns project#creator_id as author_id when is not a GitLab user' do
+ expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id as author_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+ end
+ end
+
+ describe '#has_comments?' do
+ context 'when number of comments is greater than zero' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(comments: 1)) }
+
+ it 'returns true' do
+ expect(issue.has_comments?).to eq true
+ end
+ end
+
+ context 'when number of comments is equal to zero' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(comments: 0)) }
+
+ it 'returns false' do
+ expect(issue.has_comments?).to eq false
+ end
+ end
+ end
+
+ describe '#number' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
+
+ it 'returns pull request number' do
+ expect(issue.number).to eq 1347
+ end
+ end
+
+ describe '#valid?' do
+ context 'when mention a pull request' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: OpenStruct.new)) }
+
+ it 'returns false' do
+ expect(issue.valid?).to eq false
+ end
+ end
+
+ context 'when does not mention a pull request' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: nil)) }
+
+ it 'returns true' do
+ expect(issue.valid?).to eq true
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
new file mode 100644
index 00000000000..e49dcb42342
--- /dev/null
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
+ let(:project) { create(:project) }
+ let(:repository) { OpenStruct.new(id: 1, fork: false) }
+ let(:source_repo) { repository }
+ let(:source_branch) { OpenStruct.new(ref: 'feature', repo: source_repo) }
+ let(:target_repo) { repository }
+ let(:target_branch) { OpenStruct.new(ref: 'master', repo: target_repo) }
+ let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:base_data) do
+ {
+ number: 1347,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil
+ }
+ end
+
+ subject(:pull_request) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when pull request is open' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ state: 'opened',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when pull request is closed' do
+ let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ state: 'closed',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: closed_at
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when pull request is merged' do
+ let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') }
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', merged_at: merged_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ state: 'merged',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: merged_at
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when it is assigned to someone' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) }
+
+ it 'returns nil as assignee_id when is not a GitLab user' do
+ expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
+ end
+
+ it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+
+ it 'returns project#creator_id as author_id when is not a GitLab user' do
+ expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id as author_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+ end
+ end
+
+ describe '#number' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
+
+ it 'returns pull request number' do
+ expect(pull_request.number).to eq 1347
+ end
+ end
+
+ describe '#valid?' do
+ let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') }
+
+ context 'when source, and target repositories are the same' do
+ context 'and source and target branches exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) }
+
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
+ end
+ end
+
+ context 'and source branch doesn not exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.valid?).to eq false
+ end
+ end
+
+ context 'and target branch doesn not exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.valid?).to eq false
+ end
+ end
+ end
+
+ context 'when source repo is a fork' do
+ let(:source_repo) { OpenStruct.new(id: 2, fork: true) }
+ let(:raw_data) { OpenStruct.new(base_data) }
+
+ it 'returns false' do
+ expect(pull_request.valid?).to eq false
+ end
+ end
+
+ context 'when target repo is a fork' do
+ let(:target_repo) { OpenStruct.new(id: 2, fork: true) }
+ let(:raw_data) { OpenStruct.new(base_data) }
+
+ it 'returns false' do
+ expect(pull_request.valid?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
new file mode 100644
index 00000000000..aed2aa39e3a
--- /dev/null
+++ b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::WikiFormatter, lib: true do
+ let(:project) do
+ create(:project, namespace: create(:namespace, path: 'gitlabhq'),
+ import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git')
+ end
+
+ subject(:wiki) { described_class.new(project)}
+
+ describe '#path_with_namespace' do
+ it 'appends .wiki to project path' do
+ expect(wiki.path_with_namespace).to eq 'gitlabhq/gitlabhq.wiki'
+ end
+ end
+
+ describe '#import_url' do
+ it 'returns URL of the wiki repository' do
+ expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
new file mode 100644
index 00000000000..1620eb6c60a
--- /dev/null
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::Highlight, lib: true do
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+ let(:commit) { project.commit(sample_commit.id) }
+
+ describe '.highlight_lines' do
+ let(:lines) do
+ Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb')
+ end
+
+ it 'should properly highlight all the lines' do
+ expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
+ expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
+ expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
+ end
+ end
+
+end
diff --git a/spec/lib/gitlab/inline_diff_spec.rb b/spec/lib/gitlab/inline_diff_spec.rb
deleted file mode 100644
index c690c195112..00000000000
--- a/spec/lib/gitlab/inline_diff_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::InlineDiff, lib: true do
- describe '#processing' do
- let(:diff) do
- <<eos
---- a/test.rb
-+++ b/test.rb
-@@ -1,6 +1,6 @@
- class Test
- def cleanup_string(input)
- return nil if input.nil?
-- input.sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip
-+ input.to_s.sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip
- end
- end
-eos
- end
-
- let(:expected) do
- ["--- a/test.rb\n",
- "+++ b/test.rb\n",
- "@@ -1,6 +1,6 @@\n",
- " class Test\n",
- " def cleanup_string(input)\n",
- " return nil if input.nil?\n",
- "- input.#!idiff-start!##!idiff-finish!#sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip\n",
- "+ input.#!idiff-start!#to_s.#!idiff-finish!#sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip\n",
- " end\n",
- " end\n"]
- end
-
- let(:subject) { Gitlab::InlineDiff.processing(diff.lines) }
-
- it 'should retain backslashes' do
- expect(subject).to eq(expected)
- end
- end
-end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index a628d0c0157..32a19bf344b 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -13,64 +13,58 @@ describe Gitlab::LDAP::Access, lib: true do
end
it { is_expected.to be_falsey }
-
+
it 'should block user in GitLab' do
access.allowed?
expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
end
end
context 'when the user is found' do
before do
- allow(Gitlab::LDAP::Person).
- to receive(:find_by_dn).and_return(:ldap_user)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
end
context 'and the user is disabled via active directory' do
before do
- allow(Gitlab::LDAP::Person).
- to receive(:disabled_via_active_directory?).and_return(true)
+ allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true)
end
it { is_expected.to be_falsey }
- it "should block user in GitLab" do
+ it 'should block user in GitLab' do
access.allowed?
expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
end
end
context 'and has no disabled flag in active diretory' do
before do
- user.block
-
- allow(Gitlab::LDAP::Person).
- to receive(:disabled_via_active_directory?).and_return(false)
+ allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false)
end
it { is_expected.to be_truthy }
context 'when auto-created users are blocked' do
-
before do
- allow_any_instance_of(Gitlab::LDAP::Config).
- to receive(:block_auto_created_users).and_return(true)
+ user.block
end
- it "does not unblock user in GitLab" do
+ it 'does not unblock user in GitLab' do
access.allowed?
expect(user).to be_blocked
+ expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end
end
- context "when auto-created users are not blocked" do
-
+ context 'when auto-created users are not blocked' do
before do
- allow_any_instance_of(Gitlab::LDAP::Config).
- to receive(:block_auto_created_users).and_return(false)
+ user.ldap_block
end
- it "should unblock user in GitLab" do
+ it 'should unblock user in GitLab' do
access.allowed?
expect(user).not_to be_blocked
end
@@ -80,8 +74,7 @@ describe Gitlab::LDAP::Access, lib: true do
context 'without ActiveDirectory enabled' do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
- allow_any_instance_of(Gitlab::LDAP::Config).
- to receive(:active_directory).and_return(false)
+ allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false)
end
it { is_expected.to be_truthy }
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 1e755259dae..03199a2523e 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -37,7 +37,7 @@ describe Gitlab::LDAP::User, lib: true do
end
it "dont marks existing ldap user as changed" do
- create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain')
+ create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', ldap_email: true)
expect(ldap_user.changed?).to be_falsey
end
end
@@ -110,6 +110,32 @@ describe Gitlab::LDAP::User, lib: true do
end
end
+ describe 'updating email' do
+ context "when LDAP sets an email" do
+ it "has a real email" do
+ expect(ldap_user.gl_user.email).to eq(info[:email])
+ end
+
+ it "has ldap_email set to true" do
+ expect(ldap_user.gl_user.ldap_email?).to be(true)
+ end
+ end
+
+ context "when LDAP doesn't set an email" do
+ before do
+ info.delete(:email)
+ end
+
+ it "has a temp email" do
+ expect(ldap_user.gl_user.temp_oauth_email?).to be(true)
+ end
+
+ it "has ldap_email set to false" do
+ expect(ldap_user.gl_user.ldap_email?).to be(false)
+ end
+ end
+ end
+
describe 'blocking' do
def configure_block(value)
allow_any_instance_of(Gitlab::LDAP::Config).
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index a7eab9d11cc..ad4290c43bb 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -48,6 +48,9 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
+ expect(transaction).to receive(:increment).
+ with(:method_duration, a_kind_of(Numeric))
+
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash),
method: 'Dummy.foo')
@@ -63,6 +66,16 @@ describe Gitlab::Metrics::Instrumentation do
@dummy.foo
end
+
+ it 'generates a method with the correct arity when using methods without arguments' do
+ dummy = Class.new do
+ def self.test; end
+ end
+
+ described_class.instrument_method(dummy, :test)
+
+ expect(dummy.method(:test).arity).to eq(0)
+ end
end
describe 'with metrics disabled' do
@@ -102,6 +115,9 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
+ expect(transaction).to receive(:increment).
+ with(:method_duration, a_kind_of(Numeric))
+
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash),
method: 'Dummy#bar')
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index aa76315c79c..f718d536130 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -37,9 +37,6 @@ describe Gitlab::Metrics::Metric do
it 'includes the tags' do
expect(hash[:tags]).to be_an_instance_of(Hash)
-
- expect(hash[:tags][:hostname]).to be_an_instance_of(String)
- expect(hash[:tags][:process_type]).to be_an_instance_of(String)
end
it 'includes the values' do
diff --git a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb b/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb
deleted file mode 100644
index 2b681c9fe34..00000000000
--- a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Metrics::ObfuscatedSQL do
- describe '#to_s' do
- it 'replaces newlines with a space' do
- sql = described_class.new("SELECT x\nFROM y")
-
- expect(sql.to_s).to eq('SELECT x FROM y')
- end
-
- describe 'using single values' do
- it 'replaces a single integer' do
- sql = described_class.new('SELECT x FROM y WHERE a = 10')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
-
- it 'replaces a single float' do
- sql = described_class.new('SELECT x FROM y WHERE a = 10.5')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
-
- it 'replaces a single quoted string' do
- sql = described_class.new("SELECT x FROM y WHERE a = 'foo'")
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
-
- if Gitlab::Database.mysql?
- it 'replaces a double quoted string' do
- sql = described_class.new('SELECT x FROM y WHERE a = "foo"')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
- end
-
- it 'replaces a single regular expression' do
- sql = described_class.new('SELECT x FROM y WHERE a = /foo/')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
-
- it 'replaces regular expressions using escaped slashes' do
- sql = described_class.new('SELECT x FROM y WHERE a = /foo\/bar/')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
- end
-
- describe 'using consecutive values' do
- it 'replaces multiple integers' do
- sql = described_class.new('SELECT x FROM y WHERE z IN (10, 20, 30)')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)')
- end
-
- it 'replaces multiple floats' do
- sql = described_class.new('SELECT x FROM y WHERE z IN (1.5, 2.5, 3.5)')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)')
- end
-
- it 'replaces multiple single quoted strings' do
- sql = described_class.new("SELECT x FROM y WHERE z IN ('foo', 'bar')")
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
- end
-
- if Gitlab::Database.mysql?
- it 'replaces multiple double quoted strings' do
- sql = described_class.new('SELECT x FROM y WHERE z IN ("foo", "bar")')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
- end
- end
-
- it 'replaces multiple regular expressions' do
- sql = described_class.new('SELECT x FROM y WHERE z IN (/foo/, /bar/)')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
- end
- end
-
- if Gitlab::Database.postgresql?
- it 'replaces double quotes' do
- sql = described_class.new('SELECT "x" FROM "y"')
-
- expect(sql.to_s).to eq('SELECT x FROM y')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index a143fe4cfcd..b99be4e1060 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -40,9 +40,9 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction)
end
- it 'tags the transaction with the request method and URI' do
- expect(transaction.tags[:request_method]).to eq('GET')
- expect(transaction.tags[:request_uri]).to eq('/foo')
+ it 'stores the request method and URI in the transaction as values' do
+ expect(transaction.values[:request_method]).to eq('GET')
+ expect(transaction.values[:request_uri]).to eq('/foo')
end
end
@@ -57,7 +57,7 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.tag_controller(transaction, env)
- expect(transaction.tags[:action]).to eq('TestController#show')
+ expect(transaction.action).to eq('TestController#show')
end
end
end
diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb
index 51a941c48cd..38da77adc9f 100644
--- a/spec/lib/gitlab/metrics/sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/sampler_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::Metrics::Sampler do
describe '#start' do
it 'gathers a sample at a given interval' do
- expect(sampler).to receive(:sleep).with(5)
+ expect(sampler).to receive(:sleep).with(a_kind_of(Numeric))
expect(sampler).to receive(:sample)
expect(sampler).to receive(:loop).and_yield
@@ -51,8 +51,8 @@ describe Gitlab::Metrics::Sampler do
expect(Gitlab::Metrics::System).to receive(:memory_usage).
and_return(9000)
- expect(Gitlab::Metrics::Metric).to receive(:new).
- with('memory_usage', value: 9000).
+ expect(sampler).to receive(:add_metric).
+ with(/memory_usage/, value: 9000).
and_call_original
sampler.sample_memory_usage
@@ -64,8 +64,8 @@ describe Gitlab::Metrics::Sampler do
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count).
and_return(4)
- expect(Gitlab::Metrics::Metric).to receive(:new).
- with('file_descriptors', value: 4).
+ expect(sampler).to receive(:add_metric).
+ with(/file_descriptors/, value: 4).
and_call_original
sampler.sample_file_descriptors
@@ -74,8 +74,8 @@ describe Gitlab::Metrics::Sampler do
describe '#sample_objects' do
it 'adds a metric containing the amount of allocated objects' do
- expect(Gitlab::Metrics::Metric).to receive(:new).
- with('object_counts', an_instance_of(Hash), an_instance_of(Hash)).
+ expect(sampler).to receive(:add_metric).
+ with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
at_least(:once).
and_call_original
@@ -87,11 +87,53 @@ describe Gitlab::Metrics::Sampler do
it 'adds a metric containing garbage collection statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
- expect(Gitlab::Metrics::Metric).to receive(:new).
- with('gc_statistics', an_instance_of(Hash)).
+ expect(sampler).to receive(:add_metric).
+ with(/gc_statistics/, an_instance_of(Hash)).
and_call_original
sampler.sample_gc
end
end
+
+ describe '#add_metric' do
+ it 'prefixes the series name for a Rails process' do
+ expect(sampler).to receive(:sidekiq?).and_return(false)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('rails_cats', { value: 10 }, {}).
+ and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+
+ it 'prefixes the series name for a Sidekiq process' do
+ expect(sampler).to receive(:sidekiq?).and_return(true)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('sidekiq_cats', { value: 10 }, {}).
+ and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+ end
+
+ describe '#sleep_interval' do
+ it 'returns a Numeric' do
+ expect(sampler.sleep_interval).to be_a_kind_of(Numeric)
+ end
+
+ # Testing random behaviour is very hard, so treat this test as a basic smoke
+ # test instead of a very accurate behaviour/unit test.
+ it 'does not return the same interval twice in a row' do
+ last = nil
+
+ 100.times do
+ interval = sampler.sleep_interval
+
+ expect(interval).to_not eq(last)
+
+ last = interval
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index 5882e7d81c7..e520a968999 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -5,22 +5,15 @@ describe Gitlab::Metrics::SidekiqMiddleware do
describe '#call' do
it 'tracks the transaction' do
- worker = Class.new.new
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
+ expect(Gitlab::Metrics::Transaction).to receive(:new).
+ with('TestWorker#perform').
+ and_call_original
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, 'test', :test) { nil }
end
end
-
- describe '#tag_worker' do
- it 'adds the worker class and action to the transaction' do
- trans = Gitlab::Metrics::Transaction.new
- worker = double(:worker, class: double(:class, name: 'TestWorker'))
-
- expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform')
-
- middleware.tag_worker(trans, worker)
- end
- end
end
diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
index c6cd584663f..0695c5ce096 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -14,19 +14,15 @@ describe Gitlab::Metrics::Subscribers::ActionView do
before do
allow(subscriber).to receive(:current_transaction).and_return(transaction)
-
- allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
- and_return(['app/views/x.html.haml', 4])
end
describe '#render_template' do
it 'tracks rendering of a template' do
values = { duration: 2.1 }
- tags = {
- view: 'app/views/x.html.haml',
- file: 'app/views/x.html.haml',
- line: 4
- }
+ tags = { view: 'app/views/x.html.haml' }
+
+ expect(transaction).to receive(:increment).
+ with(:view_duration, 2.1)
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, values, tags)
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 05b6cc14716..7bc070a4d09 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -2,31 +2,34 @@ require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActiveRecord do
let(:transaction) { Gitlab::Metrics::Transaction.new }
-
- let(:subscriber) { described_class.new }
+ let(:subscriber) { described_class.new }
let(:event) do
double(:event, duration: 0.2,
payload: { sql: 'SELECT * FROM users WHERE id = 10' })
end
- before do
- allow(subscriber).to receive(:current_transaction).and_return(transaction)
+ describe '#sql' do
+ describe 'without a current transaction' do
+ it 'simply returns' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to_not receive(:increment)
- allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
- and_return(['app/models/foo.rb', 4])
- end
+ subscriber.sql(event)
+ end
+ end
- describe '#sql' do
- it 'tracks the execution of a SQL query' do
- sql = 'SELECT * FROM users WHERE id = ?'
- values = { duration: 0.2 }
- tags = { sql: sql, file: 'app/models/foo.rb', line: 4 }
+ describe 'with a current transaction' do
+ it 'increments the :sql_duration value' do
+ expect(subscriber).to receive(:current_transaction).
+ at_least(:once).
+ and_return(transaction)
- expect(transaction).to receive(:add_metric).
- with(described_class::SERIES, values, tags)
+ expect(transaction).to receive(:increment).
+ with(:sql_duration, 0.2)
- subscriber.sql(event)
+ subscriber.sql(event)
+ end
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 6862fc9e2d1..1d5a51a157e 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -11,6 +11,14 @@ describe Gitlab::Metrics::Transaction do
end
end
+ describe '#allocated_memory' do
+ it 'returns the allocated memory in bytes' do
+ transaction.run { 'a' * 32 }
+
+ expect(transaction.allocated_memory).to be_a_kind_of(Numeric)
+ end
+ end
+
describe '#run' do
it 'yields the supplied block' do
expect { |b| transaction.run(&b) }.to yield_control
@@ -30,14 +38,45 @@ describe Gitlab::Metrics::Transaction do
end
describe '#add_metric' do
- it 'adds a metric tagged with the transaction UUID' do
+ it 'adds a metric to the transaction' do
expect(Gitlab::Metrics::Metric).to receive(:new).
- with('foo', { number: 10 }, { transaction_id: transaction.uuid })
+ with('rails_foo', { number: 10 }, {})
transaction.add_metric('foo', number: 10)
end
end
+ describe '#increment' do
+ it 'increments a counter' do
+ transaction.increment(:time, 1)
+ transaction.increment(:time, 2)
+
+ values = { duration: 0.0, time: 3, allocated_memory: a_kind_of(Numeric) }
+
+ expect(transaction).to receive(:add_metric).
+ with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#set' do
+ it 'sets a value' do
+ transaction.set(:number, 10)
+
+ values = {
+ duration: 0.0,
+ number: 10,
+ allocated_memory: a_kind_of(Numeric)
+ }
+
+ expect(transaction).to receive(:add_metric).
+ with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
describe '#add_tag' do
it 'adds a tag' do
transaction.add_tag(:foo, 'bar')
@@ -57,8 +96,13 @@ describe Gitlab::Metrics::Transaction do
describe '#track_self' do
it 'adds a metric for the transaction itself' do
+ values = {
+ duration: transaction.duration,
+ allocated_memory: a_kind_of(Numeric)
+ }
+
expect(transaction).to receive(:add_metric).
- with(described_class::SERIES, { duration: transaction.duration }, {})
+ with('transactions', values, {})
transaction.track_self
end
@@ -73,5 +117,22 @@ describe Gitlab::Metrics::Transaction do
transaction.submit
end
+
+ it 'adds the action as a tag for every metric' do
+ transaction.action = 'Foo#bar'
+ transaction.track_self
+
+ hash = {
+ series: 'rails_transactions',
+ tags: { action: 'Foo#bar' },
+ values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) },
+ timestamp: an_instance_of(Fixnum)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([hash])
+
+ transaction.submit
+ end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 6c0682cac4d..0ec8a6dc5cb 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -1,15 +1,9 @@
require 'spec_helper'
describe Gitlab::Metrics do
- describe '.pool_size' do
- it 'returns a Fixnum' do
- expect(described_class.pool_size).to be_an_instance_of(Fixnum)
- end
- end
-
- describe '.timeout' do
- it 'returns a Fixnum' do
- expect(described_class.timeout).to be_an_instance_of(Fixnum)
+ describe '.settings' do
+ it 'returns a Hash' do
+ expect(described_class.settings).to be_an_instance_of(Hash)
end
end
@@ -19,21 +13,6 @@ describe Gitlab::Metrics do
end
end
- describe '.hostname' do
- it 'returns a String containing the hostname' do
- expect(described_class.hostname).to eq(Socket.gethostname)
- end
- end
-
- describe '.last_relative_application_frame' do
- it 'returns an Array containing a file path and line number' do
- file, line = described_class.last_relative_application_frame
-
- expect(line).to eq(__LINE__ - 2)
- expect(file).to eq('spec/lib/gitlab/metrics_spec.rb')
- end
- end
-
describe '#submit_metrics' do
it 'prepares and writes the metrics to InfluxDB' do
connection = double(:connection)
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
new file mode 100644
index 00000000000..117a15264da
--- /dev/null
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::Middleware::Go, lib: true do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+
+ describe '#call' do
+ describe 'when go-get=0' do
+ it 'skips go-import generation' do
+ env = { 'rack.input' => '',
+ 'QUERY_STRING' => 'go-get=0' }
+ expect(app).to receive(:call).with(env).and_return('no-go')
+ middleware.call(env)
+ end
+ end
+
+ describe 'when go-get=1' do
+ it 'returns a document' do
+ env = { 'rack.input' => '',
+ 'QUERY_STRING' => 'go-get=1',
+ 'PATH_INFO' => '/group/project/path' }
+ resp = middleware.call(env)
+ expect(resp[0]).to eq(200)
+ expect(resp[1]['Content-Type']).to eq('text/html')
+ expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n"
+ expect(resp[2].body).to eq([expected_body])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb
index 6cbdae737f4..da652677443 100644
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ b/spec/lib/gitlab/note_data_builder_spec.rb
@@ -16,58 +16,80 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
end
describe 'When asking for a note on commit' do
- let(:note) { create(:note_on_commit) }
+ let(:note) { create(:note_on_commit, project: project) }
it 'returns the note and commit-specific data' do
expect(data).to have_key(:commit)
end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
describe 'When asking for a note on commit diff' do
- let(:note) { create(:note_on_commit_diff) }
+ let(:note) { create(:note_on_commit_diff, project: project) }
it 'returns the note and commit-specific data' do
expect(data).to have_key(:commit)
end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
describe 'When asking for a note on issue' do
let(:issue) { create(:issue, created_at: fixed_time, updated_at: fixed_time) }
- let(:note) { create(:note_on_issue, noteable_id: issue.id) }
+ let(:note) { create(:note_on_issue, noteable_id: issue.id, project: project) }
it 'returns the note and issue-specific data' do
expect(data).to have_key(:issue)
- expect(data[:issue]).to eq(issue.hook_attrs)
+ expect(data[:issue].except('updated_at')).to eq(issue.hook_attrs.except('updated_at'))
+ expect(data[:issue]['updated_at']).to be > issue.hook_attrs['updated_at']
end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
describe 'When asking for a note on merge request' do
let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) }
- let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) }
+ let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id, project: project) }
it 'returns the note and merge request data' do
expect(data).to have_key(:merge_request)
- expect(data[:merge_request]).to eq(merge_request.hook_attrs)
+ expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at'))
+ expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at']
end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
describe 'When asking for a note on merge request diff' do
let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) }
- let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) }
+ let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id, project: project) }
it 'returns the note and merge request diff data' do
expect(data).to have_key(:merge_request)
- expect(data[:merge_request]).to eq(merge_request.hook_attrs)
+ expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at'))
+ expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at']
end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
describe 'When asking for a note on project snippet' do
let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) }
- let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) }
+ let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id, project: project) }
it 'returns the note and project snippet data' do
expect(data).to have_key(:snippet)
- expect(data[:snippet]).to eq(snippet.hook_attrs)
+ expect(data[:snippet].except('updated_at')).to eq(snippet.hook_attrs.except('updated_at'))
+ expect(data[:snippet]['updated_at']).to be > snippet.hook_attrs['updated_at']
end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 925bc442a90..3a769acfdc0 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -41,7 +41,20 @@ describe Gitlab::OAuth::User, lib: true do
describe 'signup' do
shared_examples "to verify compliance with allow_single_sign_on" do
- context "with allow_single_sign_on enabled" do
+ context "with new allow_single_sign_on enabled syntax" do
+ before { stub_omniauth_config(allow_single_sign_on: ['twitter']) }
+
+ it "creates a user from Omniauth" do
+ oauth_user.save
+
+ expect(gl_user).to be_valid
+ identity = gl_user.identities.first
+ expect(identity.extern_uid).to eql uid
+ expect(identity.provider).to eql 'twitter'
+ end
+ end
+
+ context "with old allow_single_sign_on enabled syntax" do
before { stub_omniauth_config(allow_single_sign_on: true) }
it "creates a user from Omniauth" do
@@ -54,7 +67,14 @@ describe Gitlab::OAuth::User, lib: true do
end
end
- context "with allow_single_sign_on disabled (Default)" do
+ context "with new allow_single_sign_on disabled syntax" do
+ before { stub_omniauth_config(allow_single_sign_on: []) }
+ it "throws an error" do
+ expect{ oauth_user.save }.to raise_error StandardError
+ end
+ end
+
+ context "with old allow_single_sign_on disabled (Default)" do
before { stub_omniauth_config(allow_single_sign_on: false) }
it "throws an error" do
expect{ oauth_user.save }.to raise_error StandardError
@@ -135,7 +155,7 @@ describe Gitlab::OAuth::User, lib: true do
describe 'blocking' do
let(:provider) { 'twitter' }
- before { stub_omniauth_config(allow_single_sign_on: true) }
+ before { stub_omniauth_config(allow_single_sign_on: ['twitter']) }
context 'signup with omniauth only' do
context 'dont block on create' do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index efc2e5f4ef1..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.id, 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.id, 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/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb
index 3ef61685398..961022b9d12 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/push_data_builder_spec.rb
@@ -1,34 +1,32 @@
require 'spec_helper'
-describe 'Gitlab::PushDataBuilder', lib: true do
+describe Gitlab::PushDataBuilder, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
- describe :build_sample do
- let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ describe '.build_sample' do
+ let(:data) { described_class.build_sample(project, user) }
it { expect(data).to be_a(Hash) }
it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
it { expect(data[:ref]).to eq('refs/heads/master') }
it { expect(data[:commits].size).to eq(3) }
- it { expect(data[:repository][:git_http_url]).to eq(project.http_url_to_repo) }
- it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) }
- it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) }
it { expect(data[:total_commits_count]).to eq(3) }
it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) }
it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) }
it { expect(data[:commits].first[:removed]).to eq([]) }
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
- describe :build do
+ describe '.build' do
let(:data) do
- Gitlab::PushDataBuilder.build(project,
- user,
- Gitlab::Git::BLANK_SHA,
- '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
- 'refs/tags/v1.1.0')
+ described_class.build(project, user, Gitlab::Git::BLANK_SHA,
+ '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
+ 'refs/tags/v1.1.0')
end
it { expect(data).to be_a(Hash) }
@@ -38,5 +36,10 @@ describe 'Gitlab::PushDataBuilder', lib: true do
it { expect(data[:ref]).to eq('refs/tags/v1.1.0') }
it { expect(data[:commits]).to be_empty }
it { expect(data[:total_commits_count]).to be_zero }
+
+ it 'does not raise an error when given nil commits' do
+ expect { described_class.build(spy, spy, spy, spy, spy, nil) }.
+ not_to raise_error
+ end
end
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7d963795e17..65af37e24f1 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)
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index d67ee423b9b..c51b10bdc69 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -21,4 +21,12 @@ describe Gitlab::Regex, lib: true do
it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) }
it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) }
end
+
+ describe 'file name regex' do
+ it { expect('foo@bar').to match(Gitlab::Regex.file_name_regex) }
+ end
+
+ describe 'file path regex' do
+ it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) }
+ end
end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
new file mode 100644
index 00000000000..de7cd99d49d
--- /dev/null
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -0,0 +1,271 @@
+require 'spec_helper'
+
+describe Gitlab::Saml::User, lib: true do
+ let(:saml_user) { described_class.new(auth_hash) }
+ let(:gl_user) { saml_user.gl_user }
+ let(:uid) { 'my-uid' }
+ let(:provider) { 'saml' }
+ let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
+ let(:info_hash) do
+ {
+ name: 'John',
+ email: 'john@mail.com'
+ }
+ end
+ let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
+
+ describe '#save' do
+ def stub_omniauth_config(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(messages)
+ end
+
+ def stub_ldap_config(messages)
+ allow(Gitlab::LDAP::Config).to receive_messages(messages)
+ end
+
+ describe 'account exists on server' do
+ before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) }
+ context 'and should bind with SAML' do
+ let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
+ it 'adds the SAML identity to the existing user' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to eq existing_user
+ identity = gl_user.identities.first
+ expect(identity.extern_uid).to eql uid
+ expect(identity.provider).to eql 'saml'
+ end
+ end
+ end
+
+ describe 'no account exists on server' do
+ shared_examples 'to verify compliance with allow_single_sign_on' do
+ context 'with allow_single_sign_on enabled' do
+ before { stub_omniauth_config(allow_single_sign_on: ['saml']) }
+
+ it 'creates a user from SAML' do
+ saml_user.save
+
+ expect(gl_user).to be_valid
+ identity = gl_user.identities.first
+ expect(identity.extern_uid).to eql uid
+ expect(identity.provider).to eql 'saml'
+ end
+ end
+
+ context 'with allow_single_sign_on default (["saml"])' do
+ before { stub_omniauth_config(allow_single_sign_on: ['saml']) }
+ it 'should not throw an error' do
+ expect{ saml_user.save }.not_to raise_error
+ end
+ end
+
+ context 'with allow_single_sign_on disabled' do
+ before { stub_omniauth_config(allow_single_sign_on: false) }
+ it 'should throw an error' do
+ expect{ saml_user.save }.to raise_error StandardError
+ end
+ end
+ end
+
+ context 'with auto_link_ldap_user disabled (default)' do
+ before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) }
+ include_examples 'to verify compliance with allow_single_sign_on'
+ end
+
+ context 'with auto_link_ldap_user enabled' do
+ before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) }
+
+ context 'and no LDAP provider defined' do
+ before { stub_ldap_config(providers: []) }
+
+ include_examples 'to verify compliance with allow_single_sign_on'
+ end
+
+ context 'and at least one LDAP provider is defined' do
+ before { stub_ldap_config(providers: %w(ldapmain)) }
+
+ context 'and a corresponding LDAP person' do
+ before do
+ allow(ldap_user).to receive(:uid) { uid }
+ allow(ldap_user).to receive(:username) { uid }
+ allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] }
+ allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+ end
+
+ context 'and no account for the LDAP user' do
+
+ it 'creates a user with dual LDAP and SAML identities' do
+ saml_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql uid
+ expect(gl_user.email).to eql 'johndoe@example.com'
+ expect(gl_user.identities.length).to eql 2
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: uid }
+ ])
+ end
+ end
+
+ context 'and LDAP user has an account already' do
+ let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
+ it "adds the omniauth identity to the LDAP account" do
+ saml_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'john'
+ expect(gl_user.email).to eql 'john@example.com'
+ expect(gl_user.identities.length).to eql 2
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: uid }
+ ])
+ end
+ end
+ end
+
+ context 'and no corresponding LDAP person' do
+ before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) }
+
+ include_examples 'to verify compliance with allow_single_sign_on'
+ end
+ end
+ end
+
+ end
+
+ describe 'blocking' do
+ before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) }
+
+ context 'signup with SAML only' do
+ context 'dont block on create' do
+ before { stub_omniauth_config(block_auto_created_users: false) }
+
+ it 'should not block the user' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before { stub_omniauth_config(block_auto_created_users: true) }
+
+ it 'should block user' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to be_blocked
+ end
+ end
+ end
+
+ context 'signup with linked omniauth and LDAP account' do
+ before do
+ stub_omniauth_config(auto_link_ldap_user: true)
+ allow(ldap_user).to receive(:uid) { uid }
+ allow(ldap_user).to receive(:username) { uid }
+ allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] }
+ allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
+ allow(saml_user).to receive(:ldap_person).and_return(ldap_user)
+ end
+
+ context "and no account for the LDAP user" do
+ context 'dont block on create (LDAP)' do
+ before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create (LDAP)' do
+ before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to be_blocked
+ end
+ end
+ end
+
+ context 'and LDAP user has an account already' do
+ let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
+
+ context 'dont block on create (LDAP)' do
+ before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create (LDAP)' do
+ before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+ end
+ end
+
+
+ context 'sign-in' do
+ before do
+ saml_user.save
+ saml_user.gl_user.activate
+ end
+
+ context 'dont block on create' do
+ before { stub_omniauth_config(block_auto_created_users: false) }
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before { stub_omniauth_config(block_auto_created_users: true) }
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'dont block on create (LDAP)' do
+ before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create (LDAP)' do
+ before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
new file mode 100644
index 00000000000..f4afe597e8d
--- /dev/null
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -0,0 +1,144 @@
+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') }
+
+ let!(:merge_request) do
+ create(:merge_request, source_project: project, title: 'foo')
+ end
+
+ let!(:milestone) { create(:milestone, project: project, title: 'foo') }
+ let(:results) { described_class.new(user, Project.all, 'foo') }
+
+ describe '#total_count' do
+ it 'returns the total amount of search hits' do
+ expect(results.total_count).to eq(4)
+ end
+ end
+
+ describe '#projects_count' do
+ it 'returns the total amount of projects' do
+ expect(results.projects_count).to eq(1)
+ end
+ end
+
+ describe '#issues_count' do
+ it 'returns the total amount of issues' do
+ expect(results.issues_count).to eq(1)
+ end
+ end
+
+ describe '#merge_requests_count' do
+ it 'returns the total amount of merge requests' do
+ expect(results.merge_requests_count).to eq(1)
+ end
+ end
+
+ describe '#milestones_count' do
+ it 'returns the total amount of milestones' do
+ expect(results.milestones_count).to eq(1)
+ end
+ end
+
+ describe '#empty?' do
+ it 'returns true when there are no search results' do
+ allow(results).to receive(:total_count).and_return(0)
+
+ expect(results.empty?).to eq(true)
+ end
+
+ it 'returns false when there are search results' 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/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
new file mode 100644
index 00000000000..e86b9ef6a63
--- /dev/null
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::SnippetSearchResults do
+ let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
+
+ let(:results) { described_class.new(Snippet.all, 'foo') }
+
+ describe '#total_count' do
+ it 'returns the total amount of search hits' do
+ expect(results.total_count).to eq(2)
+ end
+ end
+
+ describe '#snippet_titles_count' do
+ it 'returns the amount of matched snippet titles' do
+ expect(results.snippet_titles_count).to eq(1)
+ end
+ end
+
+ describe '#snippet_blobs_count' do
+ it 'returns the amount of matched snippet blobs' do
+ expect(results.snippet_blobs_count).to eq(1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
new file mode 100644
index 00000000000..d940bf05061
--- /dev/null
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Gitlab::Workhorse, lib: true do
+ let(:project) { create(:project) }
+ let(:subject) { Gitlab::Workhorse }
+
+ describe "#send_git_archive" do
+ context "when the repository doesn't have an archive file path" do
+ before do
+ allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
+ end
+
+ it "raises an error" do
+ expect { subject.send_git_archive(project, "master", "zip") }.to raise_error(RuntimeError)
+ end
+ end
+ end
+end
diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb
new file mode 100644
index 00000000000..eb433c38873
--- /dev/null
+++ b/spec/mailers/abuse_report_mailer_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+describe AbuseReportMailer do
+ include EmailSpec::Matchers
+
+ describe '.notify' do
+ context 'with admin_notification_email set' do
+ before do
+ stub_application_setting(admin_notification_email: 'admin@example.com')
+ end
+
+ it 'sends to the admin_notification_email' do
+ report = create(:abuse_report)
+
+ mail = described_class.notify(report.id)
+
+ expect(mail).to deliver_to 'admin@example.com'
+ end
+
+ it 'includes the user in the subject' do
+ report = create(:abuse_report)
+
+ mail = described_class.notify(report.id)
+
+ expect(mail).to have_subject "#{report.user.name} (#{report.user.username}) was reported for abuse"
+ end
+ end
+
+ context 'with no admin_notification_email set' do
+ it 'returns early' do
+ stub_application_setting(admin_notification_email: nil)
+
+ expect { described_class.notify(spy).deliver_now }.
+ not_to change { ActionMailer::Base.deliveries.count }
+ end
+ end
+ end
+end
diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb
new file mode 100644
index 00000000000..0df89938e97
--- /dev/null
+++ b/spec/mailers/emails/builds_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+require 'email_spec'
+require 'mailers/shared/notify'
+
+describe Notify do
+ include EmailSpec::Matchers
+
+ include_context 'gitlab email notification'
+
+ describe 'build notification email' do
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+
+ shared_examples 'build email' do
+ it 'contains name of project' do
+ is_expected.to have_body_text build.project_name
+ end
+
+ it 'contains link to project' do
+ is_expected.to have_body_text namespace_project_path(project.namespace, project)
+ end
+ end
+
+ shared_examples 'an email with X-GitLab headers containing build details' do
+ it 'has X-GitLab-Build* headers' do
+ is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/
+ is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/
+ end
+ end
+
+ describe 'build success' do
+ subject { Notify.build_success_email(build.id, 'wow@example.com') }
+ before { build.success }
+
+ it_behaves_like 'build email'
+ it_behaves_like 'an email with X-GitLab headers containing build details'
+ it_behaves_like 'an email with X-GitLab headers containing project details'
+
+ it 'has header indicating build status' do
+ is_expected.to have_header 'X-GitLab-Build-Status', 'success'
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /Build success for/
+ end
+ end
+
+ describe 'build fail' do
+ subject { Notify.build_fail_email(build.id, 'wow@example.com') }
+ before { build.drop }
+
+ it_behaves_like 'build email'
+ it_behaves_like 'an email with X-GitLab headers containing build details'
+ it_behaves_like 'an email with X-GitLab headers containing project details'
+
+ it 'has header indicating build status' do
+ is_expected.to have_header 'X-GitLab-Build-Status', 'failed'
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /Build failed for/
+ end
+ end
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
new file mode 100644
index 00000000000..c6758ccad39
--- /dev/null
+++ b/spec/mailers/emails/profile_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+require 'email_spec'
+require 'mailers/shared/notify'
+
+describe Notify do
+ include EmailSpec::Matchers
+ include_context 'gitlab email notification'
+
+ describe 'profile notifications' do
+ describe 'for new users, the email' do
+ let(:example_site_path) { root_path }
+ let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
+ let(:token) { 'kETLwRaayvigPq_x3SNM' }
+
+ subject { Notify.new_user_email(new_user.id, token) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'a new user email'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'contains the password text' do
+ is_expected.to have_body_text /Click here to set your password/
+ end
+
+ it 'includes a link for user to set password' do
+ params = "reset_password_token=#{token}"
+ is_expected.to have_body_text(
+ %r{http://localhost(:\d+)?/users/password/edit\?#{params}}
+ )
+ end
+
+ it 'explains the reset link expiration' do
+ is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/)
+ is_expected.to have_body_text(new_user_password_url)
+ is_expected.to have_body_text(/\?user_email=.*%40.*/)
+ end
+ end
+
+ describe 'for users that signed up, the email' do
+ let(:example_site_path) { root_path }
+ let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
+
+ subject { Notify.new_user_email(new_user.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'a new user email'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'should not contain the new user\'s password' do
+ is_expected.not_to have_body_text /password/
+ end
+ end
+
+ describe 'user added ssh key' do
+ let(:key) { create(:personal_key) }
+
+ subject { Notify.new_ssh_key_email(key.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'is sent to the new user' do
+ is_expected.to deliver_to key.user.email
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /^SSH key was added to your account$/i
+ end
+
+ it 'contains the new ssh key title' do
+ is_expected.to have_body_text /#{key.title}/
+ end
+
+ it 'includes a link to ssh keys page' do
+ is_expected.to have_body_text /#{profile_keys_path}/
+ end
+
+ context 'with SSH key that does not exist' do
+ it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error }
+ end
+ end
+
+ describe 'user added email' do
+ let(:email) { create(:email) }
+
+ subject { Notify.new_email_email(email.id) }
+
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'is sent to the new user' do
+ is_expected.to deliver_to email.user.email
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /^Email was added to your account$/i
+ end
+
+ it 'contains the new email address' do
+ is_expected.to have_body_text /#{email.email}/
+ end
+
+ it 'includes a link to emails page' do
+ is_expected.to have_body_text /#{profile_emails_path}/
+ end
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 154901a2fbc..f910424d85b 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1,203 +1,13 @@
require 'spec_helper'
require 'email_spec'
+require 'mailers/shared/notify'
describe Notify do
include EmailSpec::Helpers
include EmailSpec::Matchers
include RepoHelpers
- new_user_address = 'newguy@example.com'
-
- let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
- let(:gitlab_sender) { Gitlab.config.gitlab.email_from }
- let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
- let(:recipient) { create(:user, email: 'recipient@example.com') }
- let(:project) { create(:project) }
- let(:build) { create(:ci_build) }
-
- before(:each) do
- ActionMailer::Base.deliveries.clear
- email = recipient.emails.create(email: "notifications@example.com")
- recipient.update_attribute(:notification_email, email.email)
- end
-
- shared_examples 'a multiple recipients email' do
- it 'is sent to the given recipient' do
- is_expected.to deliver_to recipient.notification_email
- end
- end
-
- shared_examples 'an email sent from GitLab' do
- it 'is sent from GitLab' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(gitlab_sender_display_name)
- expect(sender.address).to eq(gitlab_sender)
- end
-
- it 'has a Reply-To address' do
- reply_to = subject.header[:reply_to].addresses
- expect(reply_to).to eq([gitlab_sender_reply_to])
- end
- end
-
- shared_examples 'an email starting a new thread' do |message_id_prefix|
- it 'has a discussion identifier' do
- is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
- is_expected.to have_header 'X-GitLab-Project', /#{project.name}/
- end
- end
-
- shared_examples 'an answer to an existing thread' do |thread_id_prefix|
- it 'has a subject that begins with Re: ' do
- is_expected.to have_subject /^Re: /
- end
-
- 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}>/
- is_expected.to have_header 'X-GitLab-Project', /#{project.name}/
- end
- end
-
- shared_examples 'a new user email' do |user_email, site_path|
- it 'is sent to the new user' do
- is_expected.to deliver_to user_email
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /^Account was created for you$/i
- end
-
- it 'contains the new user\'s login name' do
- is_expected.to have_body_text /#{user_email}/
- end
-
- it 'includes a link to the site' do
- is_expected.to have_body_text /#{site_path}/
- end
- end
-
- shared_examples 'it should have Gmail Actions links' do
- it { is_expected.to have_body_text /ViewAction/ }
- end
-
- shared_examples 'it should not have Gmail Actions links' do
- it { is_expected.to_not have_body_text /ViewAction/ }
- end
-
- shared_examples 'it should show Gmail Actions View Issue link' do
- it_behaves_like 'it should have Gmail Actions links'
-
- it { is_expected.to have_body_text /View Issue/ }
- end
-
- shared_examples 'it should show Gmail Actions View Merge request link' do
- it_behaves_like 'it should have Gmail Actions links'
-
- it { is_expected.to have_body_text /View Merge request/ }
- end
-
- shared_examples 'it should show Gmail Actions View Commit link' do
- it_behaves_like 'it should have Gmail Actions links'
-
- it { is_expected.to have_body_text /View Commit/ }
- end
-
- describe 'for new users, the email' do
- let(:example_site_path) { root_path }
- let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
-
- token = 'kETLwRaayvigPq_x3SNM'
-
- subject { Notify.new_user_email(new_user.id, token) }
-
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'a new user email', new_user_address
- it_behaves_like 'it should not have Gmail Actions links'
-
- it 'contains the password text' do
- is_expected.to have_body_text /Click here to set your password/
- end
-
- it 'includes a link for user to set password' do
- params = "reset_password_token=#{token}"
- is_expected.to have_body_text(
- %r{http://localhost(:\d+)?/users/password/edit\?#{params}}
- )
- end
-
- it 'explains the reset link expiration' do
- is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/)
- is_expected.to have_body_text(new_user_password_url)
- is_expected.to have_body_text(/\?user_email=.*%40.*/)
- end
- end
-
-
- describe 'for users that signed up, the email' do
- let(:example_site_path) { root_path }
- let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
-
- subject { Notify.new_user_email(new_user.id) }
-
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'a new user email', new_user_address
- it_behaves_like 'it should not have Gmail Actions links'
-
- it 'should not contain the new user\'s password' do
- is_expected.not_to have_body_text /password/
- end
- end
-
- describe 'user added ssh key' do
- let(:key) { create(:personal_key) }
-
- subject { Notify.new_ssh_key_email(key.id) }
-
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'it should not have Gmail Actions links'
-
- it 'is sent to the new user' do
- is_expected.to deliver_to key.user.email
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /^SSH key was added to your account$/i
- end
-
- it 'contains the new ssh key title' do
- is_expected.to have_body_text /#{key.title}/
- end
-
- it 'includes a link to ssh keys page' do
- is_expected.to have_body_text /#{profile_keys_path}/
- end
- end
-
- describe 'user added email' do
- let(:email) { create(:email) }
-
- subject { Notify.new_email_email(email.id) }
-
- it_behaves_like 'it should not have Gmail Actions links'
-
- it 'is sent to the new user' do
- is_expected.to deliver_to email.user.email
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /^Email was added to your account$/i
- end
-
- it 'contains the new email address' do
- is_expected.to have_body_text /#{email.email}/
- end
-
- it 'includes a link to emails page' do
- is_expected.to have_body_text /#{profile_emails_path}/
- end
- end
+ include_context 'gitlab email notification'
context 'for a project' do
describe 'items that are assignable, the email' do
@@ -227,6 +37,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
@@ -235,6 +46,17 @@ describe Notify do
it 'contains a link to the new issue' do
is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
end
+
+ context 'when enabled email_author_in_body' do
+ before do
+ allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
+ end
+
+ it 'contains a link to note author' do
+ is_expected.to have_body_text issue.author_name
+ is_expected.to have_body_text /wrote\:/
+ end
+ end
end
describe 'that are new with a description' do
@@ -253,6 +75,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -277,12 +100,41 @@ describe Notify do
end
end
+ describe 'that have been relabeled' 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 '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'
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
+ end
+
+ it 'contains the names of the added labels' do
+ is_expected.to have_body_text /foo, bar, and baz/
+ end
+
+ it 'contains a link to the issue' do
+ is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+ end
+ end
+
describe 'status changed' 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 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -319,6 +171,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -339,12 +192,24 @@ describe Notify do
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)
+ end
+
+ it 'contains a link to note author' do
+ is_expected.to have_body_text merge_request.author_name
+ is_expected.to have_body_text /wrote\:/
+ end
+ end
end
describe 'that are new with a description' do
subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'contains the description' do
is_expected.to have_body_text /#{merge_request_with_description.description}/
@@ -357,6 +222,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -381,12 +247,41 @@ describe Notify do
end
end
+ describe 'that have been relabeled' 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 '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'
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ end
+
+ it 'contains the names of the added labels' do
+ is_expected.to have_body_text /foo, bar, and baz/
+ end
+
+ it 'contains a link to the merge request' do
+ is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+ end
+ end
+
describe 'status changed' 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 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -417,6 +312,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
@@ -446,6 +342,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Project was moved/
@@ -468,6 +365,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Access to project was granted/
@@ -506,6 +404,21 @@ describe Notify do
it 'contains the message from the note' do
is_expected.to have_body_text /#{note.note}/
end
+
+ it 'not contains note author' do
+ is_expected.not_to have_body_text /wrote\:/
+ end
+
+ context 'when enabled email_author_in_body' do
+ before do
+ allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
+ end
+
+ it 'contains a link to note author' do
+ is_expected.to have_body_text note.author_name
+ is_expected.to have_body_text /wrote\:/
+ end
+ end
end
describe 'on a commit' do
@@ -518,6 +431,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'commit'
it_behaves_like 'it should show Gmail Actions View Commit 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}\)/
@@ -538,6 +452,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -558,6 +473,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
@@ -579,6 +495,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Access to group was granted/
@@ -607,6 +524,7 @@ describe Notify do
subject { ActionMailer::Base.deliveries.last }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent to the new user' do
is_expected.to deliver_to 'new-email@mail.com'
@@ -629,6 +547,9 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'an email with X-GitLab headers containing project details'
+ it_behaves_like 'an email that contains a header with author username'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -657,6 +578,9 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'an email with X-GitLab headers containing project details'
+ it_behaves_like 'an email that contains a header with author username'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -684,6 +608,9 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'an email with X-GitLab headers containing project details'
+ it_behaves_like 'an email that contains a header with author username'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -707,6 +634,9 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'an email with X-GitLab headers containing project details'
+ it_behaves_like 'an email that contains a header with author username'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -734,6 +664,9 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'an email with X-GitLab headers containing project details'
+ it_behaves_like 'an email that contains a header with author username'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -839,6 +772,9 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
it_behaves_like 'it should show Gmail Actions View Commit link'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'an email with X-GitLab headers containing project details'
+ it_behaves_like 'an email that contains a header with author username'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -867,31 +803,4 @@ describe Notify do
end
end
- describe 'build success' do
- before { build.success }
-
- subject { Notify.build_success_email(build.id, 'wow@example.com') }
-
- it 'has the correct subject' do
- should have_subject /Build success for/
- end
-
- it 'contains name of project' do
- should have_body_text build.project_name
- end
- end
-
- describe 'build fail' do
- before { build.drop }
-
- subject { Notify.build_fail_email(build.id, 'wow@example.com') }
-
- it 'has the correct subject' do
- should have_subject /Build failed for/
- end
-
- it 'contains name of project' do
- should have_body_text build.project_name
- end
- end
end
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
new file mode 100644
index 00000000000..6019af544d3
--- /dev/null
+++ b/spec/mailers/shared/notify.rb
@@ -0,0 +1,121 @@
+shared_context 'gitlab email notification' do
+ let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
+ let(:gitlab_sender) { Gitlab.config.gitlab.email_from }
+ let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
+ let(:recipient) { create(:user, email: 'recipient@example.com') }
+ let(:project) { create(:project) }
+ let(:new_user_address) { 'newguy@example.com' }
+
+ before do
+ ActionMailer::Base.deliveries.clear
+ email = recipient.emails.create(email: "notifications@example.com")
+ recipient.update_attribute(:notification_email, email.email)
+ end
+end
+
+shared_examples 'a multiple recipients email' do
+ it 'is sent to the given recipient' do
+ is_expected.to deliver_to recipient.notification_email
+ end
+end
+
+shared_examples 'an email sent from GitLab' do
+ it 'is sent from GitLab' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(gitlab_sender_display_name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has a Reply-To address' do
+ reply_to = subject.header[:reply_to].addresses
+ expect(reply_to).to eq([gitlab_sender_reply_to])
+ end
+end
+
+shared_examples 'an email that contains a header with author username' do
+ it 'has X-GitLab-Author header containing author\'s username' do
+ is_expected.to have_header 'X-GitLab-Author', user.username
+ end
+end
+
+shared_examples 'an email with X-GitLab headers containing project details' do
+ it 'has X-GitLab-Project* headers' do
+ is_expected.to have_header 'X-GitLab-Project', /#{project.name}/
+ is_expected.to have_header 'X-GitLab-Project-Id', /#{project.id}/
+ is_expected.to have_header 'X-GitLab-Project-Path', /#{project.path_with_namespace}/
+ 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'
+
+ it 'has a discussion identifier' do
+ is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
+ end
+end
+
+shared_examples 'an answer to an existing thread' do |thread_id_prefix|
+ include_examples 'an email with X-GitLab headers containing project details'
+
+ it 'has a subject that begins with Re: ' do
+ is_expected.to have_subject /^Re: /
+ end
+
+ 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}>/
+ end
+end
+
+shared_examples 'a new user email' do
+ it 'is sent to the new user' do
+ is_expected.to deliver_to new_user_address
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /^Account was created for you$/i
+ end
+
+ it 'contains the new user\'s login name' do
+ is_expected.to have_body_text /#{new_user_address}/
+ end
+end
+
+shared_examples 'it should have Gmail Actions links' do
+ it { is_expected.to have_body_text /ViewAction/ }
+end
+
+shared_examples 'it should not have Gmail Actions links' do
+ it { is_expected.to_not have_body_text /ViewAction/ }
+end
+
+shared_examples 'it should show Gmail Actions View Issue link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Issue/ }
+end
+
+shared_examples 'it should show Gmail Actions View Merge request link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Merge request/ }
+end
+
+shared_examples 'it should show Gmail Actions View Commit link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Commit/ }
+end
+
+shared_examples 'an unsubscribeable thread' do
+ it { is_expected.to have_body_text /unsubscribe/ }
+end
+
+shared_examples 'a user cannot unsubscribe through footer link' do
+ it { is_expected.not_to have_body_text /unsubscribe/ }
+end
+
+shared_examples 'an email with a labels subscriptions link in its footer' do
+ it { is_expected.to have_body_text /label subscriptions/ }
+end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index d45319b25d4..ac12ab6c757 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -13,7 +13,8 @@
require 'rails_helper'
RSpec.describe AbuseReport, type: :model do
- subject { create(:abuse_report) }
+ subject { create(:abuse_report) }
+ let(:user) { create(:user) }
it { expect(subject).to be_valid }
@@ -26,6 +27,36 @@ RSpec.describe AbuseReport, type: :model do
it { is_expected.to validate_presence_of(:reporter) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:message) }
- it { is_expected.to validate_uniqueness_of(:user_id) }
+ it { is_expected.to validate_uniqueness_of(:user_id).with_message('has already been reported') }
+ end
+
+ describe '#remove_user' do
+ it 'blocks the user' do
+ expect { subject.remove_user(deleted_by: user) }.to change { subject.user.blocked? }.to(true)
+ end
+
+ it 'lets a worker delete the user' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id,
+ delete_solo_owned_groups: true)
+
+ subject.remove_user(deleted_by: user)
+ end
+ end
+
+ describe '#notify' do
+ it 'delivers' do
+ expect(AbuseReportMailer).to receive(:notify).with(subject.id).
+ and_return(spy)
+
+ subject.notify
+ end
+
+ it 'returns early when not persisted' do
+ report = build(:abuse_report)
+
+ expect(AbuseReportMailer).not_to receive(:notify)
+
+ report.notify
+ end
end
end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
new file mode 100644
index 00000000000..c5658bd26e1
--- /dev/null
+++ b/spec/models/appearance_spec.rb
@@ -0,0 +1,10 @@
+require 'rails_helper'
+
+RSpec.describe Appearance, type: :model do
+ subject { create(:appearance) }
+
+ it { is_expected.to be_valid }
+
+ it { is_expected.to validate_presence_of(:title) }
+ it { is_expected.to validate_presence_of(:description) }
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 35d8220ae54..b1764d7ac09 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -2,32 +2,47 @@
#
# Table name: application_settings
#
-# id :integer not null, primary key
-# default_projects_limit :integer
-# signup_enabled :boolean
-# signin_enabled :boolean
-# gravatar_enabled :boolean
-# sign_in_text :text
-# created_at :datetime
-# 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
-# restricted_signup_domains :text
-# user_oauth_applications :boolean default(TRUE)
-# after_sign_out_path :string(255)
-# session_expire_delay :integer default(10080), not null
-# import_sources :text
-# help_page_text :text
-# admin_notification_email :string(255)
-# shared_runners_enabled :boolean default(TRUE), not null
-# max_artifacts_size :integer default(100), not null
-# runners_registration_token :string(255)
+# id :integer not null, primary key
+# default_projects_limit :integer
+# signup_enabled :boolean
+# signin_enabled :boolean
+# gravatar_enabled :boolean
+# sign_in_text :text
+# created_at :datetime
+# 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
+# restricted_signup_domains :text
+# user_oauth_applications :boolean default(TRUE)
+# after_sign_out_path :string(255)
+# session_expire_delay :integer default(10080), not null
+# import_sources :text
+# help_page_text :text
+# admin_notification_email :string(255)
+# shared_runners_enabled :boolean default(TRUE), not null
+# max_artifacts_size :integer default(100), not null
+# runners_registration_token :string
+# require_two_factor_authentication :boolean default(FALSE)
+# two_factor_grace_period :integer default(48)
+# metrics_enabled :boolean default(FALSE)
+# metrics_host :string default("localhost")
+# metrics_username :string
+# metrics_password :string
+# metrics_pool_size :integer default(16)
+# metrics_timeout :integer default(10)
+# metrics_method_call_threshold :integer default(10)
+# recaptcha_enabled :boolean default(FALSE)
+# recaptcha_site_key :string
+# recaptcha_private_key :string
+# metrics_port :integer default(8089)
+# sentry_enabled :boolean default(FALSE)
+# sentry_dsn :string
#
require 'spec_helper'
@@ -51,6 +66,18 @@ describe ApplicationSetting, models: true do
it { is_expected.to allow_value(http).for(:after_sign_out_path) }
it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
+
+ it { is_expected.to validate_presence_of(:max_attachment_size) }
+
+ it do
+ is_expected.to validate_numericality_of(:max_attachment_size)
+ .only_integer
+ .is_greater_than(0)
+ end
+
+ it_behaves_like 'an object with email-formated attributes', :admin_notification_email do
+ subject { setting }
+ end
end
context 'restricted signup domains' do
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
new file mode 100644
index 00000000000..78e95c8fac5
--- /dev/null
+++ b/spec/models/blob_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+describe Blob do
+ describe '.decorate' do
+ it 'returns NilClass when given nil' do
+ expect(described_class.decorate(nil)).to be_nil
+ end
+ end
+
+ describe '#svg?' do
+ it 'is falsey when not text' do
+ git_blob = double(text?: false)
+
+ expect(described_class.decorate(git_blob)).not_to be_svg
+ end
+
+ it 'is falsey when no language is detected' do
+ git_blob = double(text?: true, language: nil)
+
+ expect(described_class.decorate(git_blob)).not_to be_svg
+ end
+
+ it' is falsey when language is not SVG' do
+ git_blob = double(text?: true, language: double(name: 'XML'))
+
+ expect(described_class.decorate(git_blob)).not_to be_svg
+ end
+
+ it 'is truthy when language is SVG' do
+ git_blob = double(text?: true, language: double(name: 'SVG'))
+
+ expect(described_class.decorate(git_blob)).to be_svg
+ end
+ end
+
+ describe '#to_partial_path' do
+ def stubbed_blob(overrides = {})
+ overrides.reverse_merge!(
+ image?: false,
+ language: nil,
+ lfs_pointer?: false,
+ svg?: false,
+ text?: false
+ )
+
+ described_class.decorate(double).tap do |blob|
+ allow(blob).to receive_messages(overrides)
+ end
+ end
+
+ it 'handles LFS pointers' do
+ blob = stubbed_blob(lfs_pointer?: true)
+
+ expect(blob.to_partial_path).to eq 'download'
+ end
+
+ it 'handles SVGs' do
+ blob = stubbed_blob(text?: true, svg?: true)
+
+ expect(blob.to_partial_path).to eq 'image'
+ end
+
+ it 'handles images' do
+ blob = stubbed_blob(image?: true)
+
+ expect(blob.to_partial_path).to eq 'image'
+ end
+
+ it 'handles text' do
+ blob = stubbed_blob(text?: true)
+
+ expect(blob.to_partial_path).to eq 'text'
+ end
+
+ it 'defaults to download' do
+ blob = stubbed_blob
+
+ expect(blob.to_partial_path).to eq 'download'
+ end
+ end
+end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index e4cac105110..f6f84db57e6 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -6,7 +6,6 @@
# message :text not null
# starts_at :datetime
# ends_at :datetime
-# alert_type :integer
# created_at :datetime
# updated_at :datetime
# color :string(255)
@@ -16,6 +15,8 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
+ include ActiveSupport::Testing::TimeHelpers
+
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
@@ -35,20 +36,79 @@ describe BroadcastMessage, models: true do
it { is_expected.not_to allow_value('000').for(:font) }
end
- describe :current do
+ describe '.current' do
it "should return last message if time match" do
- broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow)
- expect(BroadcastMessage.current).to eq(broadcast_message)
+ message = create(:broadcast_message)
+
+ expect(BroadcastMessage.current).to eq message
end
it "should return nil if time not come" do
- create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days)
+ create(:broadcast_message, :future)
+
expect(BroadcastMessage.current).to be_nil
end
it "should return nil if time has passed" do
- create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday)
+ create(:broadcast_message, :expired)
+
expect(BroadcastMessage.current).to be_nil
end
end
+
+ describe '#active?' do
+ it 'is truthy when started and not ended' do
+ message = build(:broadcast_message)
+
+ expect(message).to be_active
+ end
+
+ it 'is falsey when ended' do
+ message = build(:broadcast_message, :expired)
+
+ expect(message).not_to be_active
+ end
+
+ it 'is falsey when not started' do
+ message = build(:broadcast_message, :future)
+
+ expect(message).not_to be_active
+ end
+ end
+
+ describe '#started?' do
+ it 'is truthy when starts_at has passed' do
+ message = build(:broadcast_message)
+
+ travel_to(3.days.from_now) do
+ expect(message).to be_started
+ end
+ end
+
+ it 'is falsey when starts_at is in the future' do
+ message = build(:broadcast_message)
+
+ travel_to(3.days.ago) do
+ expect(message).not_to be_started
+ end
+ end
+ end
+
+ describe '#ended?' do
+ it 'is truthy when ends_at has passed' do
+ message = build(:broadcast_message)
+
+ travel_to(3.days.from_now) do
+ expect(message).to be_ended
+ end
+ end
+
+ it 'is falsey when ends_at is in the future' do
+ message = build(:broadcast_message)
+
+ travel_to(3.days.ago) do
+ expect(message).not_to be_ended
+ end
+ end
+ end
end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 1c22e3cb7c4..b7457808040 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -1,32 +1,7 @@
-# == Schema Information
-#
-# Table name: builds
-#
-# id :integer not null, primary key
-# project_id :integer
-# status :string(255)
-# finished_at :datetime
-# trace :text
-# created_at :datetime
-# updated_at :datetime
-# started_at :datetime
-# runner_id :integer
-# commit_id :integer
-# coverage :float
-# commands :text
-# job_id :integer
-# name :string(255)
-# deploy :boolean default(FALSE)
-# options :text
-# allow_failure :boolean default(FALSE), not null
-# stage :string(255)
-# trigger_request_id :integer
-#
-
require 'spec_helper'
describe Ci::Build, models: true do
- let(:project) { FactoryGirl.create :empty_project }
+ let(:project) { FactoryGirl.create :project }
let(:commit) { FactoryGirl.create :ci_commit, project: project }
let(:build) { FactoryGirl.create :ci_build, commit: commit }
@@ -34,7 +9,7 @@ describe Ci::Build, models: true do
it { is_expected.to respond_to :trace_html }
- describe :first_pending do
+ describe '#first_pending' do
let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday }
let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' }
before { first; second }
@@ -44,7 +19,7 @@ describe Ci::Build, models: true do
it('returns with the first pending build') { is_expected.to eq(first) }
end
- describe :create_from do
+ describe '#create_from' do
before do
build.status = 'success'
build.save
@@ -58,7 +33,7 @@ describe Ci::Build, models: true do
end
end
- describe :ignored? do
+ describe '#ignored?' do
subject { build.ignored? }
context 'if build is not allowed to fail' do
@@ -94,7 +69,7 @@ describe Ci::Build, models: true do
end
end
- describe :trace do
+ describe '#trace' do
subject { build.trace_html }
it { is_expected.to be_empty }
@@ -126,7 +101,7 @@ describe Ci::Build, models: true do
# it { is_expected.to eq(commit.project.timeout) }
# end
- describe :options do
+ describe '#options' do
let(:options) do
{
image: "ruby:2.1",
@@ -147,25 +122,25 @@ describe Ci::Build, models: true do
# it { is_expected.to eq(project.allow_git_fetch) }
# end
- describe :project do
+ describe '#project' do
subject { build.project }
it { is_expected.to eq(commit.project) }
end
- describe :project_id do
+ describe '#project_id' do
subject { build.project_id }
it { is_expected.to eq(commit.project_id) }
end
- describe :project_name do
+ describe '#project_name' do
subject { build.project_name }
it { is_expected.to eq(project.name) }
end
- describe :extract_coverage do
+ describe '#extract_coverage' do
context 'valid content & regex' do
subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
@@ -197,7 +172,7 @@ describe Ci::Build, models: true do
end
end
- describe :variables do
+ describe '#variables' do
context 'returns variables' do
subject { build.variables }
@@ -267,8 +242,8 @@ describe Ci::Build, models: true do
end
end
- describe :can_be_served? do
- let(:runner) { FactoryGirl.create :ci_specific_runner }
+ describe '#can_be_served?' do
+ let(:runner) { FactoryGirl.create :ci_runner }
before { build.project.runners << runner }
@@ -302,7 +277,7 @@ describe Ci::Build, models: true do
end
end
- describe :any_runners_online? do
+ describe '#any_runners_online?' do
subject { build.any_runners_online? }
context 'when no runners' do
@@ -310,7 +285,7 @@ describe Ci::Build, models: true do
end
context 'if there are runner' do
- let(:runner) { FactoryGirl.create :ci_specific_runner }
+ let(:runner) { FactoryGirl.create :ci_runner }
before do
build.project.runners << runner
@@ -337,8 +312,8 @@ describe Ci::Build, models: true do
end
end
- describe :show_warning? do
- subject { build.show_warning? }
+ describe '#stuck?' do
+ subject { build.stuck? }
%w(pending).each do |state|
context "if commit_status.status is #{state}" do
@@ -347,7 +322,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_truthy }
context "and there are specific runner" do
- let(:runner) { FactoryGirl.create :ci_specific_runner, contacted_at: 1.second.ago }
+ let(:runner) { FactoryGirl.create :ci_runner, contacted_at: 1.second.ago }
before do
build.project.runners << runner
@@ -368,22 +343,34 @@ describe Ci::Build, models: true do
end
end
- describe :download_url do
- subject { build.download_url }
+ describe '#artifacts?' do
+ subject { build.artifacts? }
- it "should be nil if artifact doesn't exist" do
- build.update_attributes(artifacts_file: nil)
- is_expected.to be_nil
+ context 'artifacts archive does not exist' do
+ before { build.update_attributes(artifacts_file: nil) }
+ it { is_expected.to be_falsy }
end
- it 'should be nil if artifact exist' do
- gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
- build.update_attributes(artifacts_file: gif)
- is_expected.to_not be_nil
+ context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+ it { is_expected.to be_truthy }
+ end
+ end
+
+
+ describe '#artifacts_metadata?' do
+ subject { build.artifacts_metadata? }
+ context 'artifacts metadata does not exist' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive is a zip file and metadata exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+ it { is_expected.to be_truthy }
end
end
- describe :repo_url do
+ describe '#repo_url' do
let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project }
@@ -397,6 +384,30 @@ describe Ci::Build, models: true do
it { is_expected.to include(project.web_url[7..-1]) }
end
+ describe '#depends_on_builds' do
+ let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' }
+ let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' }
+ let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' }
+ let!(:staging) { FactoryGirl.create :ci_build, commit: commit, name: 'staging', stage_idx: 2, stage: 'deploy' }
+
+ it 'to have no dependents if this is first build' do
+ expect(build.depends_on_builds).to be_empty
+ end
+
+ it 'to have one dependent if this is test' do
+ expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
+ end
+
+ it 'to have all builds from build and test stage if this is last' do
+ expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
+ end
+
+ it 'to have retried builds instead the original ones' do
+ retried_rspec = Ci::Build.retry(rspec_test)
+ expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
+ end
+ end
+
def create_mr(build, commit, factory: :merge_request, created_at: Time.now)
FactoryGirl.create(factory,
source_project_id: commit.gl_project_id,
@@ -405,7 +416,7 @@ describe Ci::Build, models: true do
created_at: created_at)
end
- describe :merge_request do
+ describe '#merge_request' do
context 'when a MR has a reference to the commit' do
before do
@merge_request = create_mr(build, commit, factory: :merge_request)
@@ -458,6 +469,103 @@ describe Ci::Build, models: true do
expect(@build2.merge_request.id).to eq(@merge_request.id)
end
end
+ end
+
+ describe 'build erasable' do
+ shared_examples 'erasable' do
+ it 'should remove artifact file' do
+ expect(build.artifacts_file.exists?).to be_falsy
+ end
+
+ it 'should remove artifact metadata file' do
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ end
+
+ it 'should erase build trace in trace file' do
+ expect(build.trace).to be_empty
+ end
+
+ it 'should set erased to true' do
+ expect(build.erased?).to be true
+ end
+
+ it 'should set erase date' do
+ expect(build.erased_at).to_not be_falsy
+ end
+ end
+
+ context 'build is not erasable' do
+ let!(:build) { create(:ci_build) }
+
+ describe '#erase' do
+ subject { build.erase }
+ it { is_expected.to be false }
+ end
+
+ describe '#erasable?' do
+ subject { build.erasable? }
+ it { is_expected.to eq false }
+ end
+ end
+
+ context 'build is erasable' do
+ let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+
+ describe '#erase' do
+ before { build.erase(erased_by: user) }
+
+ context 'erased by user' do
+ let!(:user) { create(:user, username: 'eraser') }
+
+ include_examples 'erasable'
+
+ it 'should record user who erased a build' do
+ expect(build.erased_by).to eq user
+ end
+ end
+
+ context 'erased by system' do
+ let(:user) { nil }
+
+ include_examples 'erasable'
+
+ it 'should not set user who erased a build' do
+ expect(build.erased_by).to be_nil
+ end
+ end
+ end
+
+ describe '#erasable?' do
+ subject { build.erasable? }
+ it { is_expected.to eq true }
+ end
+
+ describe '#erased?' do
+ let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+ subject { build.erased? }
+
+ context 'build has not been erased' do
+ it { is_expected.to be false }
+ end
+
+ context 'build has been erased' do
+ before { build.erase }
+
+ it { is_expected.to be true }
+ end
+ end
+
+ context 'metadata and build trace are not available' do
+ let!(:build) { create(:ci_build, :success, :artifacts) }
+ before { build.remove_artifacts_metadata! }
+
+ describe '#erase' do
+ it 'should not raise error' do
+ expect { build.erase }.to_not raise_error
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
new file mode 100644
index 00000000000..36d10636ae9
--- /dev/null
+++ b/spec/models/ci/build_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Ci::Build, models: true do
+ let(:build) { create(:ci_build) }
+ let(:test_trace) { 'This is a test' }
+
+ describe '#trace' do
+ it 'obfuscates project runners token' do
+ allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
+
+ expect(build.trace).to eq("Test: xxxxxx")
+ end
+
+ it 'empty project runners token' do
+ allow(build).to receive(:raw_trace).and_return(test_trace)
+ # runners_token can't normally be set to nil
+ allow(build.project).to receive(:runners_token).and_return(nil)
+
+ expect(build.trace).to eq(test_trace)
+ end
+ end
+end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index b193e16e7f8..412842337ba 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -13,7 +13,7 @@
# tag :boolean default(FALSE)
# yaml_errors :text
# committed_at :datetime
-# project_id :integer
+# gl_project_id :integer
#
require 'spec_helper'
@@ -32,50 +32,6 @@ describe Ci::Commit, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
- describe :ordered do
- let(:project) { FactoryGirl.create :empty_project }
-
- it 'returns ordered list of commits' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
- expect(project.ci_commits.ordered).to eq([commit2, commit1])
- end
-
- it 'returns commits ordered by committed_at and id, with nulls last' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
- commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
- commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
- expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1])
- end
- end
-
- describe :last_build do
- subject { commit.last_build }
- before do
- @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
- @second = FactoryGirl.create :ci_build, commit: commit
- end
-
- it { is_expected.to be_a(Ci::Build) }
- it('returns with the most recently created build') { is_expected.to eq(@second) }
- end
-
- describe :retry do
- before do
- @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
- @second = FactoryGirl.create :ci_build, commit: commit
- end
-
- it "creates only a new build" do
- expect(commit.builds.count(:all)).to eq 2
- expect(commit.statuses.count(:all)).to eq 2
- commit.retry
- expect(commit.builds.count(:all)).to eq 3
- expect(commit.statuses.count(:all)).to eq 3
- end
- end
-
describe :valid_commit_sha do
context 'commit.sha can not start with 00000000' do
before do
@@ -247,6 +203,35 @@ describe Ci::Commit, models: true do
end
end
+
+ context 'custom stage with first job allowed to fail' do
+ let(:yaml) do
+ {
+ stages: ['clean', 'test'],
+ clean_job: {
+ stage: 'clean',
+ allow_failure: true,
+ script: 'BUILD',
+ },
+ test_job: {
+ stage: 'test',
+ script: 'TEST',
+ },
+ }
+ end
+
+ before do
+ stub_ci_commit_yaml_file(YAML.dump(yaml))
+ create_builds
+ end
+
+ it 'properly schedules builds' do
+ expect(commit.builds.pluck(:status)).to contain_exactly('pending')
+ commit.builds.running_or_pending.each(&:drop)
+ expect(commit.builds.pluck(:status)).to contain_exactly('pending', 'failed')
+ end
+ end
+
context 'properly creates builds when "when" is defined' do
let(:yaml) do
{
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
index da8491357a5..000a732db77 100644
--- a/spec/models/ci/runner_project_spec.rb
+++ b/spec/models/ci/runner_project_spec.rb
@@ -2,11 +2,12 @@
#
# Table name: ci_runner_projects
#
-# id :integer not null, primary key
-# runner_id :integer not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 232760dfeba..25e9e5eca48 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -39,7 +39,7 @@ describe Ci::Runner, models: true do
describe :assign_to do
let!(:project) { FactoryGirl.create :empty_project }
- let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) }
+ let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) }
before { shared_runner.assign_to(project) }
@@ -52,15 +52,15 @@ describe Ci::Runner, models: true do
subject { Ci::Runner.online }
before do
- @runner1 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.year.ago)
- @runner2 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago)
+ @runner1 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.year.ago)
+ @runner2 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago)
end
it { is_expected.to eq([@runner2])}
end
describe :online? do
- let(:runner) { FactoryGirl.create(:ci_shared_runner) }
+ let(:runner) { FactoryGirl.create(:ci_runner, :shared) }
subject { runner.online? }
@@ -84,7 +84,7 @@ describe Ci::Runner, models: true do
end
describe :status do
- let(:runner) { FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) }
+ let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
subject { runner.status }
@@ -115,7 +115,7 @@ describe Ci::Runner, models: true do
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
- runner = FactoryGirl.create(:ci_specific_runner)
+ runner = FactoryGirl.create(:ci_runner)
project = FactoryGirl.create(:empty_project)
project1 = FactoryGirl.create(:empty_project)
project.runners << runner
@@ -125,11 +125,39 @@ describe Ci::Runner, models: true do
end
it "returns true" do
- runner = FactoryGirl.create(:ci_specific_runner)
+ runner = FactoryGirl.create(:ci_runner)
project = FactoryGirl.create(:empty_project)
project.runners << runner
expect(runner.belongs_to_one_project?).to be_truthy
end
end
+
+ describe '#search' do
+ let(:runner) { create(:ci_runner, token: '123abc') }
+
+ it 'returns runners with a matching token' do
+ expect(described_class.search(runner.token)).to eq([runner])
+ end
+
+ it 'returns runners with a partially matching token' do
+ expect(described_class.search(runner.token[0..2])).to eq([runner])
+ end
+
+ it 'returns runners with a matching token regardless of the casing' do
+ expect(described_class.search(runner.token.upcase)).to eq([runner])
+ end
+
+ it 'returns runners with a matching description' do
+ expect(described_class.search(runner.description)).to eq([runner])
+ end
+
+ it 'returns runners with a partially matching description' do
+ expect(described_class.search(runner.description[0..2])).to eq([runner])
+ end
+
+ it 'returns runners with a matching description regardless of the casing' do
+ expect(described_class.search(runner.description.upcase)).to eq([runner])
+ end
+ end
end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index cb2f51e2011..159be939300 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -2,12 +2,13 @@
#
# Table name: ci_triggers
#
-# id :integer not null, primary key
-# token :string(255)
-# project_id :integer not null
-# deleted_at :datetime
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 31b56953a13..71e84091cb7 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -3,12 +3,13 @@
# Table name: ci_variables
#
# id :integer not null, primary key
-# project_id :integer not null
+# project_id :integer
# key :string(255)
# value :text
# encrypted_value :text
# encrypted_value_salt :string(255)
# encrypted_value_iv :string(255)
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index ecf37b40c58..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
@@ -118,4 +129,38 @@ eos
it { expect(data[:modified]).to eq([".gitmodules"]) }
it { expect(data[:removed]).to eq([]) }
end
+
+ describe '#reverts_commit?' do
+ let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") }
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+
+ context 'commit has no description' do
+ before { allow(commit).to receive(:description?).and_return(false) }
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ end
+
+ context "another_commit's description does not revert commit" do
+ before { allow(commit).to receive(:description).and_return("Foo Bar") }
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_falsy }
+ end
+
+ context "another_commit's description reverts commit" do
+ before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") }
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
+ end
+
+ context "another_commit's description reverts merged merge request" do
+ before do
+ revert_description = "This reverts merge request !foo123"
+ allow(another_commit).to receive(:revert_description).and_return(revert_description)
+ allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar")
+ end
+
+ it { expect(commit.reverts_commit?(another_commit)).to be_truthy }
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index b8f901b3433..82c68ff6cb1 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -29,6 +29,7 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb
index 25b3f4e50da..92fdc5cd65d 100644
--- a/spec/models/concerns/case_sensitivity_spec.rb
+++ b/spec/models/concerns/case_sensitivity_spec.rb
@@ -37,7 +37,7 @@ describe CaseSensitivity, models: true do
with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar').
and_return(criteria)
- expect(model.iwhere(:'foo.bar' => 'bar')).to eq(criteria)
+ expect(model.iwhere('foo.bar'.to_sym => 'bar')).to eq(criteria)
end
end
@@ -87,8 +87,8 @@ describe CaseSensitivity, models: true do
with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz').
and_return(final)
- got = model.iwhere(:'foo.bar' => 'bar',
- :'foo.baz' => 'baz')
+ got = model.iwhere('foo.bar'.to_sym => 'bar',
+ 'foo.baz'.to_sym => 'baz')
expect(got).to eq(final)
end
@@ -127,7 +127,7 @@ describe CaseSensitivity, models: true do
with(%q{`foo`.`bar` = :value}, value: 'bar').
and_return(criteria)
- expect(model.iwhere(:'foo.bar' => 'bar')).
+ expect(model.iwhere('foo.bar'.to_sym => 'bar')).
to eq(criteria)
end
end
@@ -178,8 +178,8 @@ describe CaseSensitivity, models: true do
with(%q{`foo`.`baz` = :value}, value: 'baz').
and_return(final)
- got = model.iwhere(:'foo.bar' => 'bar',
- :'foo.baz' => 'baz')
+ got = model.iwhere('foo.bar'.to_sym => 'bar',
+ 'foo.baz'.to_sym => 'baz')
expect(got).to eq(final)
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 021d62cdf0c..be29b6d66ff 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -32,9 +32,54 @@ describe Issue, "Issuable" do
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
- it "matches by title" do
+ it 'returns notes with a matching title' do
+ expect(described_class.search(searchable_issue.title)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a partially matching title' do
expect(described_class.search('able')).to eq([searchable_issue])
end
+
+ it 'returns notes with a matching title regardless of the casing' do
+ expect(described_class.search(searchable_issue.title.upcase)).
+ to eq([searchable_issue])
+ end
+ end
+
+ describe ".full_search" do
+ let!(:searchable_issue) do
+ create(:issue, title: "Searchable issue", description: 'kittens')
+ end
+
+ it 'returns notes with a matching title' do
+ expect(described_class.full_search(searchable_issue.title)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a partially matching title' do
+ expect(described_class.full_search('able')).to eq([searchable_issue])
+ end
+
+ it 'returns notes with a matching title regardless of the casing' do
+ expect(described_class.full_search(searchable_issue.title.upcase)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a matching description' do
+ expect(described_class.full_search(searchable_issue.description)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a partially matching description' do
+ expect(described_class.full_search(searchable_issue.description)).
+ to eq([searchable_issue])
+ end
+
+ it 'returns notes with a matching description regardless of the casing' do
+ expect(described_class.full_search(searchable_issue.description.upcase)).
+ to eq([searchable_issue])
+ end
end
describe "#today?" do
@@ -68,18 +113,71 @@ describe Issue, "Issuable" do
end
end
+ describe '#subscribed?' do
+ context 'user is not a participant in the issue' do
+ before { allow(issue).to receive(:participants).with(user).and_return([]) }
+
+ it 'returns false when no subcription exists' do
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ issue.subscriptions.create(user: user, subscribed: true)
+
+ expect(issue.subscribed?(user)).to be_truthy
+ end
+
+ it 'returns false when a subcription exists and subscribed is false' do
+ issue.subscriptions.create(user: user, subscribed: false)
+
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+ end
+
+ context 'user is a participant in the issue' do
+ before { allow(issue).to receive(:participants).with(user).and_return([user]) }
+
+ it 'returns false when no subcription exists' do
+ expect(issue.subscribed?(user)).to be_truthy
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ issue.subscriptions.create(user: user, subscribed: true)
+
+ expect(issue.subscribed?(user)).to be_truthy
+ end
+
+ it 'returns false when a subcription exists and subscribed is false' do
+ issue.subscriptions.create(user: user, subscribed: false)
+
+ expect(issue.subscribed?(user)).to be_falsey
+ end
+ end
+ end
+
describe "#to_hook_data" do
- let(:hook_data) { issue.to_hook_data(user) }
+ let(:data) { issue.to_hook_data(user) }
+ let(:project) { issue.project }
+
it "returns correct hook data" do
- expect(hook_data[:object_kind]).to eq("issue")
- expect(hook_data[:user]).to eq(user.hook_attrs)
- expect(hook_data[:repository][:name]).to eq(issue.project.name)
- expect(hook_data[:repository][:url]).to eq(issue.project.url_to_repo)
- expect(hook_data[:repository][:description]).to eq(issue.project.description)
- expect(hook_data[:repository][:homepage]).to eq(issue.project.web_url)
- expect(hook_data[:object_attributes]).to eq(issue.hook_attrs)
+ expect(data[:object_kind]).to eq("issue")
+ expect(data[:user]).to eq(user.hook_attrs)
+ expect(data[:object_attributes]).to eq(issue.hook_attrs)
+ expect(data).to_not have_key(:assignee)
end
+
+ context "issue is assigned" do
+ before { issue.update_attribute(:assignee, user) }
+
+ it "returns correct hook data" do
+ expect(data[:object_attributes]['assignee_id']).to eq(user.id)
+ expect(data[:assignee]).to eq(user.hook_attrs)
+ end
+ end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
describe '#card_attributes' 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/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
new file mode 100644
index 00000000000..e31fdb0bffb
--- /dev/null
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Subscribable, 'Subscribable' do
+ let(:resource) { create(:issue) }
+ let(:user) { create(:user) }
+
+ describe '#subscribed?' do
+ it 'returns false when no subcription exists' do
+ expect(resource.subscribed?(user)).to be_falsey
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ resource.subscriptions.create(user: user, subscribed: true)
+
+ expect(resource.subscribed?(user)).to be_truthy
+ end
+
+ it 'returns false when a subcription exists and subscribed is false' do
+ resource.subscriptions.create(user: user, subscribed: false)
+
+ expect(resource.subscribed?(user)).to be_falsey
+ end
+ end
+ describe '#subscribers' do
+ it 'returns [] when no subcribers exists' do
+ expect(resource.subscribers).to be_empty
+ end
+
+ it 'returns the subscribed users' do
+ resource.subscriptions.create(user: user, subscribed: true)
+ resource.subscriptions.create(user: create(:user), subscribed: false)
+
+ expect(resource.subscribers).to eq [user]
+ end
+ end
+
+ describe '#toggle_subscription' do
+ it 'toggles the current subscription state for the given user' do
+ expect(resource.subscribed?(user)).to be_falsey
+
+ resource.toggle_subscription(user)
+
+ expect(resource.subscribed?(user)).to be_truthy
+ end
+ end
+
+ describe '#unsubscribe' do
+ it 'unsubscribes the given current user' do
+ resource.subscriptions.create(user: user, subscribed: true)
+ expect(resource.subscribed?(user)).to be_truthy
+
+ resource.unsubscribe(user)
+
+ expect(resource.subscribed?(user)).to be_falsey
+ end
+ end
+end
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
new file mode 100644
index 00000000000..a20a6149649
--- /dev/null
+++ b/spec/models/email_spec.rb
@@ -0,0 +1,22 @@
+# == Schema Information
+#
+# Table name: emails
+#
+# id :integer not null, primary key
+# user_id :integer not null
+# email :string(255) not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+require 'spec_helper'
+
+describe Email, models: true do
+
+ describe 'validations' do
+ it_behaves_like 'an object with email-formated attributes', :email do
+ subject { build(:email) }
+ end
+ end
+
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 071582b0282..5fe44246738 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -65,23 +65,38 @@ describe Event, models: true do
it { expect(@event.author).to eq(@user) }
end
- describe '.latest_update_time' do
- describe 'when events are present' do
- let(:time) { Time.utc(2015, 1, 1) }
+ describe '#proper?' do
+ context 'issue event' 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(:event) { Event.new(project: project, action: Event::CREATED, target: issue, author_id: author.id) }
before do
- create(:closed_issue_event, updated_at: time)
- create(:closed_issue_event, updated_at: time + 5)
+ project.team << [member, :developer]
end
- it 'returns the latest update time' do
- expect(Event.latest_update_time).to eq(time + 5)
+ context 'for non confidential issues' do
+ let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
+
+ it { expect(event.proper?(non_member)).to eq true }
+ it { expect(event.proper?(author)).to eq true }
+ it { expect(event.proper?(assignee)).to eq true }
+ it { expect(event.proper?(member)).to eq true }
+ it { expect(event.proper?(admin)).to eq true }
end
- end
- describe 'when no events exist' do
- it 'returns nil' do
- expect(Event.latest_update_time).to be_nil
+ context 'for confidential issues' do
+ let(:issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+
+ it { expect(event.proper?(non_member)).to eq false }
+ it { expect(event.proper?(author)).to eq true }
+ it { expect(event.proper?(assignee)).to eq true }
+ it { expect(event.proper?(member)).to eq true }
+ it { expect(event.proper?(admin)).to eq true }
end
end
end
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index 6ec6b9037a4..9b144dd1ecc 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -10,6 +10,21 @@ describe ExternalIssue, models: true do
it { is_expected.to include_module(Referable) }
end
+ describe '.reference_pattern' do
+ it 'allows underscores in the project name' do
+ expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ end
+
+ it 'allows numbers in the project name' do
+ expect(ExternalIssue.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
+ end
+
+ it 'requires the project name to begin with A-Z' do
+ expect(ExternalIssue.reference_pattern.match('3EXT_EXT-1234')).to eq nil
+ expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(issue.to_reference).to eq issue.id
diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb
index b198aa77526..d37978720bf 100644
--- a/spec/models/external_wiki_service_spec.rb
+++ b/spec/models/external_wiki_service_spec.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require 'spec_helper'
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index d61c1c96bde..5b0883d8702 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -29,6 +29,7 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 646f767e6fe..c9245fc9535 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
require 'spec_helper'
@@ -38,14 +37,6 @@ describe Group, models: true do
it { is_expected.not_to validate_presence_of :owner }
end
- describe '.public_and_given_groups' do
- let!(:public_group) { create(:group, public: true) }
-
- subject { described_class.public_and_given_groups([group.id]) }
-
- it { is_expected.to eq([public_group, group]) }
- end
-
describe '.visible_to_user' do
let!(:group) { create(:group) }
let!(:user) { create(:user) }
@@ -113,22 +104,29 @@ describe Group, models: true do
end
end
- describe "public_profile?" do
- it "returns true for public group" do
- group = create(:group, public: true)
- expect(group.public_profile?).to be_truthy
+ describe '.search' do
+ it 'returns groups with a matching name' do
+ expect(described_class.search(group.name)).to eq([group])
+ end
+
+ it 'returns groups with a partially matching name' do
+ expect(described_class.search(group.name[0..2])).to eq([group])
+ end
+
+ it 'returns groups with a matching name regardless of the casing' do
+ expect(described_class.search(group.name.upcase)).to eq([group])
+ end
+
+ it 'returns groups with a matching path' do
+ expect(described_class.search(group.path)).to eq([group])
end
- it "returns true for non-public group with public project" do
- group = create(:group)
- create(:project, :public, group: group)
- expect(group.public_profile?).to be_truthy
+ it 'returns groups with a partially matching path' do
+ expect(described_class.search(group.path[0..2])).to eq([group])
end
- it "returns false for non-public group with no public projects" do
- group = create(:group)
- create(:project, group: group)
- expect(group.public_profile?).to be_falsy
+ it 'returns groups with a matching path regardless of the casing' do
+ expect(described_class.search(group.path.upcase)).to eq([group])
end
end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 645ee0b929a..983848392b7 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -19,6 +19,10 @@
require 'spec_helper'
describe ProjectHook, models: true do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
describe '.push_hooks' do
it 'should return hooks for push events only' do
hook = create(:project_hook, push_events: true)
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 1455661485b..f800f415bd2 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -31,7 +31,7 @@ describe ServiceHook, models: true do
WebMock.stub_request(:post, @service_hook.url)
end
- it "POSTs to the web hook URL" do
+ it "POSTs to the webhook URL" do
@service_hook.execute(@data)
expect(WebMock).to have_requested(:post, @service_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' }
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 138b87a9a06..fd1513cab1b 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -36,7 +36,7 @@ describe SystemHook, models: true do
it "project_destroy hook" do
user = create(:user)
project = create(:empty_project, namespace: user.namespace)
- Projects::DestroyService.new(project, user, {}).execute
+ Projects::DestroyService.new(project, user, {}).pending_delete!
expect(WebMock).to have_requested(:post, @system_hook.url).with(
body: /project_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
@@ -65,7 +65,7 @@ describe SystemHook, models: true do
project = create(:project)
project.team << [user, :master]
expect(WebMock).to have_requested(:post, @system_hook.url).with(
- body: /user_add_to_team/,
+ body: /user_add_to_team/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
@@ -76,7 +76,7 @@ describe SystemHook, models: true do
project.team << [user, :master]
project.project_members.destroy_all
expect(WebMock).to have_requested(:post, @system_hook.url).with(
- body: /user_remove_from_team/,
+ body: /user_remove_from_team/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 2d90b0793cc..04bc2dcfb16 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -18,20 +18,14 @@
require 'spec_helper'
-describe ProjectHook, models: true do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- end
-
- describe "Mass assignment" do
- end
-
+describe WebHook, models: true do
describe "Validations" do
it { is_expected.to validate_presence_of(:url) }
- context "url format" do
+ describe 'url' do
it { is_expected.to allow_value("http://example.com").for(:url) }
- it { is_expected.to allow_value("https://excample.com").for(:url) }
+ it { is_expected.to allow_value("https://example.com").for(:url) }
+ it { is_expected.to allow_value(" https://example.com ").for(:url) }
it { is_expected.to allow_value("http://test.com/api").for(:url) }
it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
@@ -39,6 +33,12 @@ describe ProjectHook, models: true do
it { is_expected.not_to allow_value("example.com").for(:url) }
it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
+
+ it 'strips :url before saving it' do
+ hook = create(:project_hook, url: ' https://example.com ')
+
+ expect(hook.url).to eq('https://example.com')
+ end
end
end
@@ -52,7 +52,7 @@ describe ProjectHook, models: true do
WebMock.stub_request(:post, @project_hook.url)
end
- it "POSTs to the web hook URL" do
+ it "POSTs to the webhook URL" do
@project_hook.execute(@data, 'push_hooks')
expect(WebMock).to have_requested(:post, @project_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' }
@@ -77,5 +77,17 @@ describe ProjectHook, models: true do
expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error'])
end
+
+ it "handles 200 status code" do
+ WebMock.stub_request(:post, @project_hook.url).to_return(status: 200, body: "Success")
+
+ expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success'])
+ end
+
+ it "handles 2xx status codes" do
+ WebMock.stub_request(:post, @project_hook.url).to_return(status: 201, body: "Success")
+
+ expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success'])
+ end
end
end
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
new file mode 100644
index 00000000000..5afe042e154
--- /dev/null
+++ b/spec/models/identity_spec.rb
@@ -0,0 +1,38 @@
+# == Schema Information
+#
+# Table name: identities
+#
+# id :integer not null, primary key
+# extern_uid :string(255)
+# provider :string(255)
+# user_id :integer
+# created_at :datetime
+# updated_at :datetime
+#
+
+require 'spec_helper'
+
+RSpec.describe Identity, models: true do
+
+ describe 'relations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'fields' do
+ it { is_expected.to respond_to(:provider) }
+ it { is_expected.to respond_to(:extern_uid) }
+ end
+
+ describe '#is_ldap?' do
+ let(:ldap_identity) { create(:identity, provider: 'ldapmain') }
+ let(:other_identity) { create(:identity, provider: 'twitter') }
+
+ it 'returns true if it is a ldap identity' do
+ expect(ldap_identity.ldap?).to be_truthy
+ end
+
+ it 'returns false if it is not a ldap identity' do
+ expect(other_identity.ldap?).to be_falsey
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 52271c7c8c6..2ccdec1eeff 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -105,6 +105,40 @@ describe Issue, models: true do
end
end
+ describe '#referenced_merge_requests' do
+ it 'returns the referenced merge requests' do
+ project = create(:project, :public)
+
+ mr1 = create(:merge_request,
+ source_project: project,
+ source_branch: 'master',
+ target_branch: 'feature')
+
+ mr2 = create(:merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_branch: 'master')
+
+ issue = create(:issue, description: mr1.to_reference, project: project)
+
+ create(:note_on_issue,
+ noteable: issue,
+ note: mr2.to_reference,
+ project_id: project.id)
+
+ expect(issue.referenced_merge_requests).to eq([mr1, mr2])
+ end
+ end
+
+ describe '#related_branches' do
+ it "should " do
+ allow(subject.project.repository).to receive(:branch_names).
+ and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name])
+
+ expect(subject.related_branches).to eq [subject.to_branch_name]
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
subject { create(:issue) }
@@ -115,4 +149,12 @@ describe Issue, models: true do
it_behaves_like 'a Taskable' do
let(:subject) { create :issue }
end
+
+ describe "#to_branch_name" do
+ let(:issue) { build(:issue, title: 'a' * 30) }
+
+ it "starts with the issue iid" do
+ expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/
+ end
+ end
end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 696fbf7e0aa..0614ca1e7c9 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -59,18 +59,42 @@ describe Label, models: true do
context 'using id' do
it 'returns a String reference to the object' do
expect(label.to_reference).to eq "~#{label.id}"
- expect(label.to_reference(double('project'))).to eq "~#{label.id}"
end
end
context 'using name' do
it 'returns a String reference to the object' do
- expect(label.to_reference(:name)).to eq %(~"#{label.name}")
+ expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
end
it 'uses id when name contains double quote' do
label = create(:label, name: %q{"irony"})
- expect(label.to_reference(:name)).to eq "~#{label.id}"
+ expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using invalid format' do
+ it 'raises error' do
+ expect { label.to_reference(format: :invalid) }
+ .to raise_error StandardError, /Unknown format/
+ end
+ end
+
+ context 'cross project reference' do
+ let(:project) { create(:project) }
+
+ context 'using name' do
+ it 'returns cross reference with label name' do
+ expect(label.to_reference(project, format: :name))
+ .to eq %Q(#{label.project.to_reference}~"#{label.name}")
+ end
+ end
+
+ context 'using id' do
+ it 'returns cross reference with label id' do
+ expect(label.to_reference(project, format: :id))
+ .to eq %Q(#{label.project.to_reference}~#{label.id})
+ end
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2aedca20df2..2d8f1cc1ad3 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -31,6 +31,10 @@ describe Member, models: true do
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
+ it_behaves_like 'an object with email-formated attributes', :invite_email do
+ subject { build(:project_member) }
+ end
+
context "when an invite email is provided" do
let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) }
@@ -159,7 +163,7 @@ describe Member, models: true do
describe "#generate_invite_token" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
-
+
it "sets the invite token" do
expect { member.generate_invite_token }.to change { member.invite_token}
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index e0653a8327d..654c71b6825 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2,25 +2,29 @@
#
# Table name: merge_requests
#
-# id :integer not null, primary key
-# target_branch :string(255) not null
-# source_branch :string(255) not null
-# source_project_id :integer not null
-# author_id :integer
-# assignee_id :integer
-# title :string(255)
-# created_at :datetime
-# updated_at :datetime
-# milestone_id :integer
-# state :string(255)
-# merge_status :string(255)
-# target_project_id :integer not null
-# iid :integer
-# description :text
-# position :integer default(0)
-# locked_at :datetime
-# updated_by_id :integer
-# merge_error :string(255)
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# source_project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# created_at :datetime
+# updated_at :datetime
+# milestone_id :integer
+# state :string(255)
+# merge_status :string(255)
+# target_project_id :integer not null
+# iid :integer
+# description :text
+# position :integer default(0)
+# locked_at :datetime
+# updated_by_id :integer
+# merge_error :string(255)
+# merge_params :text
+# merge_when_build_succeeds :boolean default(FALSE), not null
+# merge_user_id :integer
+# merge_commit_sha :string
#
require 'spec_helper'
@@ -76,6 +80,37 @@ describe MergeRequest, models: true do
it { is_expected.to respond_to(:merge_when_build_succeeds) }
end
+ describe '.in_projects' do
+ it 'returns the merge requests for a set of projects' do
+ expect(described_class.in_projects(Project.all)).to eq([subject])
+ 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}"
@@ -134,11 +169,13 @@ describe MergeRequest, models: true do
describe 'detection of issues to be closed' do
let(:issue0) { create :issue, project: subject.project }
let(:issue1) { create :issue, project: subject.project }
- let(:commit0) { double('commit0', closes_issues: [issue0]) }
- let(:commit1) { double('commit1', closes_issues: [issue0]) }
- let(:commit2) { double('commit2', closes_issues: [issue1]) }
+
+ let(:commit0) { double('commit0', safe_message: "Fixes #{issue0.to_reference}") }
+ let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") }
+ 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
@@ -146,7 +183,9 @@ describe MergeRequest, models: true do
allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch)
- expect(subject.closes_issues).to eq([issue0, issue1].sort_by(&:id))
+ closed = subject.closes_issues
+
+ expect(closed).to include(issue0, issue1)
end
it 'only lists issues as to be closed if it targets the default branch' do
@@ -164,17 +203,6 @@ describe MergeRequest, models: true do
expect(subject.closes_issues).to include(issue2)
end
-
- context 'for a project with JIRA integration' do
- let(:issue0) { JiraIssue.new('JIRA-123', subject.project) }
- let(:issue1) { JiraIssue.new('FOOBAR-4567', subject.project) }
-
- it 'returns sorted JiraIssues' do
- allow(subject.project).to receive_messages(default_branch: subject.target_branch)
-
- expect(subject.closes_issues).to eq([issue0, issue1])
- end
- end
end
describe "#work_in_progress?" do
@@ -193,6 +221,11 @@ describe MergeRequest, models: true do
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 "doesn't detect WIP for words starting with WIP" do
subject.title = "Wipwap #{subject.title}"
expect(subject).not_to be_work_in_progress
@@ -231,9 +264,15 @@ describe MergeRequest, models: true do
expect(subject.can_remove_source_branch?(user2)).to be_falsey
end
- it "is can be removed in all other cases" do
+ it "can be removed if the last commit is the head of the source branch" do
+ allow(subject.source_project).to receive(:commit).and_return(subject.last_commit)
+
expect(subject.can_remove_source_branch?(user)).to be_truthy
end
+
+ it "cannot be removed if the last commit is not also the head of the source branch" do
+ expect(subject.can_remove_source_branch?(user)).to be_falsey
+ end
end
describe "#reset_merge_when_build_succeeds" do
@@ -248,13 +287,86 @@ describe MergeRequest, models: true do
end
describe "#hook_attrs" do
+ let(:attrs_hash) { subject.hook_attrs.to_h }
+
+ [:source, :target].each do |key|
+ describe "#{key} key" do
+ include_examples 'project hook data', project_key: key do
+ let(:data) { attrs_hash }
+ let(:project) { subject.send("#{key}_project") }
+ end
+ end
+ end
+
it "has all the required keys" do
- attrs = subject.hook_attrs
- attrs = attrs.to_h
- expect(attrs).to include(:source)
- expect(attrs).to include(:target)
- expect(attrs).to include(:last_commit)
- expect(attrs).to include(:work_in_progress)
+ expect(attrs_hash).to include(:source)
+ expect(attrs_hash).to include(:target)
+ expect(attrs_hash).to include(:last_commit)
+ expect(attrs_hash).to include(:work_in_progress)
+ end
+ end
+
+ describe '#diverged_commits_count' do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+
+ context 'diverged on same repository' do
+ subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }
+
+ it 'counts commits that are on target branch but not on source branch' do
+ expect(subject.diverged_commits_count).to eq(5)
+ end
+ end
+
+ context 'diverged on fork' do
+ subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) }
+
+ it 'counts commits that are on target branch but not on source branch' do
+ expect(subject.diverged_commits_count).to eq(5)
+ end
+ end
+
+ context 'rebased on fork' do
+ subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) }
+
+ it 'counts commits that are on target branch but not on source branch' do
+ expect(subject.diverged_commits_count).to eq(0)
+ end
+ end
+
+ describe 'caching' do
+ before(:example) do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+ end
+
+ it 'caches the output' do
+ expect(subject).to receive(:compute_diverged_commits_count).
+ once.
+ and_return(2)
+
+ subject.diverged_commits_count
+ subject.diverged_commits_count
+ end
+
+ it 'invalidates the cache when the source sha changes' do
+ expect(subject).to receive(:compute_diverged_commits_count).
+ twice.
+ and_return(2)
+
+ subject.diverged_commits_count
+ allow(subject).to receive(:source_sha).and_return('123abc')
+ subject.diverged_commits_count
+ end
+
+ it 'invalidates the cache when the target sha changes' do
+ expect(subject).to receive(:compute_diverged_commits_count).
+ twice.
+ and_return(2)
+
+ subject.diverged_commits_count
+ allow(subject).to receive(:target_sha).and_return('123abc')
+ subject.diverged_commits_count
+ end
end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 30a71987d86..72a4ea70228 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -32,22 +32,36 @@ 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
+ new_milestone = Milestone.new(project: milestone.project, title: milestone.title)
+ expect(new_milestone).not_to be_valid
+ end
+
+ it "should accept the same title in another project" do
+ project = build(:project)
+ new_milestone = Milestone.new(project: project, title: milestone.title)
+
+ expect(new_milestone).to be_valid
+ end
+ end
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(:count).and_return(0)
- expect(milestone.percent_complete).to eq(0)
+ expect(milestone.percent_complete(user)).to eq(0)
end
end
@@ -89,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
@@ -99,24 +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.open_items_count).to eq(2) }
- 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
@@ -168,4 +181,34 @@ describe Milestone, models: true do
expect(issue4.position).to eq(42)
end
end
+
+ describe '.search' do
+ let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }
+
+ it 'returns milestones with a matching title' do
+ expect(described_class.search(milestone.title)).to eq([milestone])
+ end
+
+ it 'returns milestones with a partially matching title' do
+ expect(described_class.search(milestone.title[0..2])).to eq([milestone])
+ end
+
+ it 'returns milestones with a matching title regardless of the casing' do
+ expect(described_class.search(milestone.title.upcase)).to eq([milestone])
+ end
+
+ it 'returns milestones with a matching description' do
+ expect(described_class.search(milestone.description)).to eq([milestone])
+ end
+
+ it 'returns milestones with a partially matching description' do
+ expect(described_class.search(milestone.description[0..2])).
+ to eq([milestone])
+ end
+
+ it 'returns milestones with a matching description regardless of the casing' do
+ expect(described_class.search(milestone.description.upcase)).
+ to eq([milestone])
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4fa2d2bc4d2..3c3a580942a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
require 'spec_helper'
@@ -42,13 +41,32 @@ describe Namespace, models: true do
it { expect(namespace.human_name).to eq(namespace.owner_name) }
end
- describe :search do
- before do
- @namespace = create :namespace
+ describe '.search' do
+ let(:namespace) { create(:namespace) }
+
+ it 'returns namespaces with a matching name' do
+ expect(described_class.search(namespace.name)).to eq([namespace])
+ end
+
+ it 'returns namespaces with a partially matching name' do
+ expect(described_class.search(namespace.name[0..2])).to eq([namespace])
+ end
+
+ it 'returns namespaces with a matching name regardless of the casing' do
+ expect(described_class.search(namespace.name.upcase)).to eq([namespace])
+ end
+
+ it 'returns namespaces with a matching path' do
+ expect(described_class.search(namespace.path)).to eq([namespace])
end
- it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) }
- it { expect(Namespace.search('unknown')).to eq([]) }
+ it 'returns namespaces with a partially matching path' do
+ expect(described_class.search(namespace.path[0..2])).to eq([namespace])
+ end
+
+ it 'returns namespaces with a matching path regardless of the casing' do
+ expect(described_class.search(namespace.path.upcase)).to eq([namespace])
+ end
end
describe :move_dir do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 593d8f76215..6b18936edb1 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -24,8 +24,10 @@ require 'spec_helper'
describe Note, models: true do
describe 'associations' do
it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:noteable) }
+ it { is_expected.to belong_to(:noteable).touch(true) }
it { is_expected.to belong_to(:author).class_name('User') }
+
+ it { is_expected.to have_many(:todos).dependent(:destroy) }
end
describe 'validation' do
@@ -125,13 +127,32 @@ describe Note, models: true do
let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
end
- describe :search do
- let!(:note) { create(:note, note: "WoW") }
+ describe "#all_references" do
+ let!(:note1) { create(:note) }
+ let!(:note2) { create(:note) }
+
+ it "reads the rendered note body from the cache" do
+ expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project)
+ expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project)
+
+ note1.all_references
+ note2.all_references
+ end
+ end
+
+ describe '.search' do
+ let(:note) { create(:note, note: 'WoW') }
+
+ it 'returns notes with matching content' do
+ expect(described_class.search(note.note)).to eq([note])
+ end
- it { expect(Note.search('wow')).to include(note) }
+ it 'returns notes with matching content regardless of the casing' do
+ expect(described_class.search('WOW')).to eq([note])
+ end
end
- describe :grouped_awards do
+ describe '.grouped_awards' do
before do
create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true
@@ -148,6 +169,66 @@ describe Note, models: true do
end
end
+ describe '#active?' do
+ it 'is always true when the note has no associated diff' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(nil)
+
+ expect(note).to be_active
+ end
+
+ it 'is never true when the note has no noteable associated' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:noteable).and_return(nil)
+
+ expect(note).not_to be_active
+ end
+
+ it 'returns the memoized value if defined' do
+ note = build(:note)
+
+ expect(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:noteable).and_return(double)
+
+ note.instance_variable_set(:@active, 'foo')
+ expect(note).not_to receive(:find_noteable_diff)
+
+ expect(note.active?).to eq 'foo'
+ end
+
+ context 'for a merge request noteable' do
+ it 'is false when noteable has no matching diff' do
+ merge = build_stubbed(:merge_request, :simple)
+ note = build(:note, noteable: merge)
+
+ allow(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:find_noteable_diff).and_return(nil)
+
+ expect(note).not_to be_active
+ end
+
+ it 'is true when noteable has a matching diff' do
+ merge = create(:merge_request, :simple)
+
+ # Generate a real line_code value so we know it will match. We use a
+ # random line from a random diff just for funsies.
+ diff = merge.diffs.to_a.sample
+ line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
+ code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
+
+ # We're persisting in order to trigger the set_diff callback
+ note = create(:note, noteable: merge, line_code: code)
+
+ # Make sure we don't get a false positive from a guard clause
+ expect(note).to receive(:find_noteable_diff).and_call_original
+ expect(note).to be_active
+ end
+ end
+ end
+
describe "editable?" do
it "returns true" do
note = build(:note)
@@ -164,13 +245,53 @@ describe Note, models: true do
expect(note.editable?).to be_falsy
end
end
-
+
+ describe "cross_reference_not_visible_for?" do
+ let(:private_user) { create(:user) }
+ let(:private_project) { create(:project, namespace: private_user.namespace).tap { |p| p.team << [private_user, :master] } }
+ let(:private_issue) { create(:issue, project: private_project) }
+
+ let(:ext_proj) { create(:project, :public) }
+ let(:ext_issue) { create(:issue, project: ext_proj) }
+
+ let(:note) do
+ create :note,
+ noteable: ext_issue, project: ext_proj,
+ note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ system: true
+ end
+
+ it "returns true" do
+ expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy
+ end
+
+ it "returns false" do
+ expect(note.cross_reference_not_visible_for?(private_user)).to be_falsy
+ end
+ end
+
describe "set_award!" do
- let(:issue) { create :issue }
+ let(:merge_request) { create :merge_request }
it "converts aliases to actual name" do
- note = create :note, note: ":+1:", noteable: issue
+ note = create(:note, note: ":+1:", noteable: merge_request)
expect(note.reload.note).to eq("thumbsup")
end
+
+ it "is not an award emoji when comment is on a diff" do
+ note = create(:note, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2")
+ note = note.reload
+
+ expect(note.note).to eq(":blowfish:")
+ expect(note.is_award?).to be_falsy
+ end
+ end
+
+ describe 'clear_blank_line_code!' do
+ it 'clears a blank line code before validation' do
+ note = build(:note, line_code: ' ')
+
+ expect { note.valid? }.to change(note, :line_code).to(nil)
+ end
end
end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
new file mode 100644
index 00000000000..2fa6715fcaf
--- /dev/null
+++ b/spec/models/project_group_link_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe ProjectGroupLink do
+ describe "Associations" do
+ it { should belong_to(:group) }
+ it { should belong_to(:project) }
+ end
+
+ describe "Validation" do
+ let!(:project_group_link) { create(:project_group_link) }
+
+ it { should validate_presence_of(:project_id) }
+ it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
+ it { should validate_presence_of(:group_id) }
+ it { should validate_presence_of(:group_access) }
+ end
+end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 64bb92fba95..f3d15f3c1ea 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -40,6 +40,20 @@ describe AsanaService, models: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
+ def create_data_for_commits(*messages)
+ {
+ object_kind: 'push',
+ ref: 'master',
+ user_name: user.name,
+ commits: messages.map do |m|
+ {
+ message: m,
+ url: 'https://gitlab.com/',
+ }
+ end
+ }
+ end
+
before do
@asana = AsanaService.new
allow(@asana).to receive_messages(
@@ -51,16 +65,67 @@ describe AsanaService, models: true do
)
end
- it 'should call Asana service to created a story' do
- expect(Asana::Task).to receive(:find).with('123456').once
+ it 'should call Asana service to create a story' do
+ data = create_data_for_commits('Message from commit. related to #123456')
+ expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
- @asana.check_commit('related to #123456', 'pushed')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment).with(text: expected_message)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '123456').once.and_return(d1)
+
+ @asana.execute(data)
end
- it 'should call Asana service to created a story and close a task' do
- expect(Asana::Task).to receive(:find).with('456789').twice
+ it 'should call Asana service to create a story and close a task' do
+ data = create_data_for_commits('fix #456789')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'should be able to close via url' do
+ data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'should allow multiple matches per line' do
+ message = <<-EOF
+ minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
+ ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
+ EOF
+ data = create_data_for_commits(message)
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
+
+ d2 = double('Asana::Task')
+ expect(d2).to receive(:add_comment)
+ expect(d2).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
+
+ d3 = double('Asana::Task')
+ expect(d3).to receive(:add_comment)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
+
+ d4 = double('Asana::Task')
+ expect(d4).to receive(:add_comment)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
+
+ d5 = double('Asana::Task')
+ expect(d5).to receive(:add_comment)
+ expect(d5).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
- @asana.check_commit('fix #456789', 'pushed')
+ @asana.execute(data)
end
end
end
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
new file mode 100644
index 00000000000..905379a64e3
--- /dev/null
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe BuildsEmailService do
+ let(:build) { create(:ci_build) }
+ let(:data) { Gitlab::BuildDataBuilder.build(build) }
+ let(:service) { BuildsEmailService.new }
+
+ describe :execute do
+ it "sends email" do
+ service.recipients = 'test@gitlab.com'
+ data[:build_status] = 'failed'
+ expect(BuildEmailWorker).to receive(:perform_async)
+ service.execute(data)
+ end
+
+ it "does not sends email with failed build and allowed_failure on" do
+ data[:build_status] = 'failed'
+ data[:build_allow_failure] = true
+ expect(BuildEmailWorker).not_to receive(:perform_async)
+ service.execute(data)
+ end
+ end
+end
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index cc92eb0bd9f..e0feb606f78 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 400bdf2d962..b8b9a455b83 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -29,6 +29,13 @@
# import_source :string(255)
# commit_count :integer default(0)
# import_error :text
+# ci_id :integer
+# builds_enabled :boolean default(TRUE), not null
+# shared_runners_enabled :boolean default(TRUE), not null
+# runners_token :string
+# build_coverage_regex :string
+# build_allow_git_fetch :boolean default(TRUE), not null
+# build_timeout :integer default(3600), not null
#
require 'spec_helper'
@@ -61,6 +68,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:todos).dependent(:destroy) }
end
describe 'modules' do
@@ -95,7 +103,7 @@ describe Project, models: true do
expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/)
end
end
-
+
describe 'project token' do
it 'should set an random token if none provided' do
project = FactoryGirl.create :empty_project, runners_token: ''
@@ -512,35 +520,35 @@ describe Project, models: true do
describe :any_runners do
let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
- let(:specific_runner) { create(:ci_specific_runner) }
- let(:shared_runner) { create(:ci_shared_runner) }
+ let(:specific_runner) { create(:ci_runner) }
+ let(:shared_runner) { create(:ci_runner, :shared) }
context 'for shared runners disabled' do
let(:shared_runners_enabled) { false }
-
+
it 'there are no runners available' do
expect(project.any_runners?).to be_falsey
end
-
+
it 'there is a specific runner' do
project.runners << specific_runner
expect(project.any_runners?).to be_truthy
end
-
+
it 'there is a shared runner, but they are prohibited to use' do
shared_runner
expect(project.any_runners?).to be_falsey
end
-
+
it 'checks the presence of specific runner' do
project.runners << specific_runner
expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
end
end
-
+
context 'for shared runners enabled' do
let(:shared_runners_enabled) { true }
-
+
it 'there is a shared runner' do
shared_runner
expect(project.any_runners?).to be_truthy
@@ -554,7 +562,7 @@ describe Project, models: true do
end
describe '#visibility_level_allowed?' do
- let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
+ let(:project) { create(:project, :internal) }
context 'when checking on non-forked project' do
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
@@ -574,6 +582,142 @@ describe Project, models: true do
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
end
+ end
+
+ describe '.search' do
+ let(:project) { create(:project, description: 'kitten mittens') }
+
+ it 'returns projects with a matching name' do
+ expect(described_class.search(project.name)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching name' do
+ expect(described_class.search(project.name[0..2])).to eq([project])
+ end
+
+ it 'returns projects with a matching name regardless of the casing' do
+ expect(described_class.search(project.name.upcase)).to eq([project])
+ end
+
+ it 'returns projects with a matching description' do
+ expect(described_class.search(project.description)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching description' do
+ expect(described_class.search('kitten')).to eq([project])
+ end
+
+ it 'returns projects with a matching description regardless of the casing' do
+ expect(described_class.search('KITTEN')).to eq([project])
+ end
+
+ it 'returns projects with a matching path' do
+ expect(described_class.search(project.path)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching path' do
+ expect(described_class.search(project.path[0..2])).to eq([project])
+ end
+
+ it 'returns projects with a matching path regardless of the casing' do
+ expect(described_class.search(project.path.upcase)).to eq([project])
+ end
+
+ it 'returns projects with a matching namespace name' do
+ expect(described_class.search(project.namespace.name)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching namespace name' do
+ expect(described_class.search(project.namespace.name[0..2])).to eq([project])
+ end
+
+ it 'returns projects with a matching namespace name regardless of the casing' do
+ expect(described_class.search(project.namespace.name.upcase)).to eq([project])
+ end
+
+ it 'returns projects when eager loading namespaces' do
+ relation = described_class.all.includes(:namespace)
+ expect(relation.search(project.namespace.name)).to eq([project])
+ end
+ end
+
+ describe '#rename_repo' do
+ let(:project) { create(:project) }
+ let(:gitlab_shell) { Gitlab::Shell.new }
+
+ before do
+ # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
+ # call. This makes testing a bit easier.
+ allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
+ end
+
+ it 'renames a repository' do
+ allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
+
+ ns = project.namespace_dir
+
+ expect(gitlab_shell).to receive(:mv_repository).
+ ordered.
+ with("#{ns}/foo", "#{ns}/#{project.path}").
+ and_return(true)
+
+ expect(gitlab_shell).to receive(:mv_repository).
+ ordered.
+ with("#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki").
+ and_return(true)
+
+ expect_any_instance_of(SystemHooksService).
+ to receive(:execute_hooks_for).
+ with(project, :rename)
+
+ expect_any_instance_of(Gitlab::UploadsTransfer).
+ to receive(:rename_project).
+ with('foo', project.path, ns)
+
+ expect(project).to receive(:expire_caches_before_rename)
+
+ project.rename_repo
+ end
+ end
+
+ describe '#expire_caches_before_rename' do
+ let(:project) { create(:project) }
+ let(:repo) { double(:repo, exists?: true) }
+ let(:wiki) { double(:wiki, exists?: true) }
+
+ it 'expires the caches of the repository and wiki' do
+ allow(Repository).to receive(:new).
+ with('foo', project).
+ and_return(repo)
+
+ allow(Repository).to receive(:new).
+ with('foo.wiki', project).
+ and_return(wiki)
+
+ expect(repo).to receive(:expire_cache)
+ expect(repo).to receive(:expire_emptiness_caches)
+
+ expect(wiki).to receive(:expire_cache)
+ expect(wiki).to receive(:expire_emptiness_caches)
+
+ project.expire_caches_before_rename('foo')
+ end
+ end
+
+ describe '.search_by_title' do
+ let(:project) { create(:project, name: 'kittens') }
+
+ it 'returns projects with a matching name' do
+ expect(described_class.search_by_title(project.name)).to eq([project])
+ end
+
+ it 'returns projects with a partially matching name' do
+ expect(described_class.search_by_title('kitten')).to eq([project])
+ end
+
+ it 'returns projects with a matching name regardless of the casing' do
+ expect(described_class.search_by_title('KITTENS')).to eq([project])
+ end
end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 5cd5ae327bf..bacb17a8883 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -67,15 +67,69 @@ describe ProjectTeam, models: true do
end
end
+ describe :max_invited_level do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+
+ group.add_user(master, Gitlab::Access::MASTER)
+ group.add_user(reporter, Gitlab::Access::REPORTER)
+ end
+
+ it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
+ end
+
+ describe :max_member_access do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+
+ group.add_user(master, Gitlab::Access::MASTER)
+ group.add_user(reporter, Gitlab::Access::REPORTER)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+
+ it "does not have an access" do
+ project.namespace.update(share_with_group_lock: true)
+ expect(project.team.max_member_access(master.id)).to be_nil
+ expect(project.team.max_member_access(reporter.id)).to be_nil
+ end
+ end
+
describe "#human_max_access" do
- it "return master role" do
- user = create :user
- group = create :group
- group.add_users([user.id], GroupMember::MASTER)
- project = create(:project, namespace: group)
- project.team << [user, :guest]
-
- expect(project.team.human_max_access(user.id)).to eq("Master")
+ it 'returns Master role' do
+ user = create(:user)
+ group = create(:group)
+ group.add_master(user)
+
+ project = build_stubbed(:empty_project, namespace: group)
+
+ expect(project.team.human_max_access(user.id)).to eq 'Master'
+ end
+
+ it 'returns Owner role' do
+ user = create(:user)
+ group = create(:group)
+ group.add_owner(user)
+
+ project = build_stubbed(:empty_project, namespace: group)
+
+ expect(project.team.human_max_access(user.id)).to eq 'Owner'
end
end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 876b927eaea..a2085df5bcd 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -36,6 +36,13 @@ describe ProjectWiki, models: true do
end
end
+ describe "#wiki_base_path" do
+ it "returns the wiki base path" do
+ wiki_base_path = "/#{project.path_with_namespace}/wikis"
+ expect(subject.wiki_base_path).to eq(wiki_base_path)
+ end
+ end
+
describe "#wiki" do
it "contains a Gollum::Wiki instance" do
expect(subject.wiki).to be_a Gollum::Wiki
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index afbf62035ac..a57229a4fdf 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -5,6 +5,15 @@ describe Repository, models: true do
let(:repository) { create(:project).repository }
let(:user) { create(:user) }
+ let(:commit_options) do
+ author = repository.user_to_committer(user)
+ { message: 'Test message', committer: author, author: author }
+ end
+ let(:merge_commit) do
+ source_sha = repository.find_branch('feature').target
+ merge_commit_id = repository.merge(user, source_sha, 'master', commit_options)
+ repository.commit(merge_commit_id)
+ end
describe :branch_names_contains do
subject { repository.branch_names_contains(sample_commit.id) }
@@ -92,13 +101,29 @@ describe Repository, models: true do
end
describe 'parsing result' do
- subject { repository.parse_search_result(results.first) }
+ subject { repository.parse_search_result(search_result) }
+ let(:search_result) { results.first }
it { is_expected.to be_an OpenStruct }
it { expect(subject.filename).to eq('CHANGELOG') }
+ it { expect(subject.basename).to eq('CHANGELOG') }
it { expect(subject.ref).to eq('master') }
it { expect(subject.startline).to eq(186) }
it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") }
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
end
end
@@ -139,6 +164,12 @@ describe Repository, models: true do
expect(branch.name).to eq('new_feature')
end
+
+ it 'calls the after_create_branch hook' do
+ expect(repository).to receive(:after_create_branch)
+
+ repository.add_branch(user, 'new_feature', 'master')
+ end
end
context 'when pre hooks failed' do
@@ -200,13 +231,22 @@ describe Repository, models: true do
describe :commit_with_hooks do
context 'when pre hooks were successful' do
- it 'should run without errors' do
- expect_any_instance_of(GitHooksService).to receive(:execute).and_return(true)
+ before do
+ expect_any_instance_of(GitHooksService).to receive(:execute).
+ and_return(true)
+ end
+ it 'should run without errors' do
expect do
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
end.not_to raise_error
end
+
+ it 'should ensure the autocrlf Git option is set to :input' do
+ expect(repository).to receive(:update_autocrlf_option)
+
+ repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ end
end
context 'when pre hooks failed' do
@@ -219,4 +259,555 @@ describe Repository, models: true do
end
end
end
+
+ describe '#exists?' do
+ it 'returns true when a repository exists' do
+ expect(repository.exists?).to eq(true)
+ end
+
+ it 'returns false when a repository does not exist' do
+ expect(repository.raw_repository).to receive(:rugged).
+ and_raise(Gitlab::Git::Repository::NoRepository)
+
+ expect(repository.exists?).to eq(false)
+ end
+
+ it 'returns false when there is no namespace' do
+ allow(repository).to receive(:path_with_namespace).and_return(nil)
+
+ expect(repository.exists?).to eq(false)
+ end
+ end
+
+ describe '#has_visible_content?' do
+ subject { repository.has_visible_content? }
+
+ describe 'when there are no branches' do
+ before do
+ allow(repository.raw_repository).to receive(:branch_count).and_return(0)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ describe 'when there are branches' do
+ it 'returns true' do
+ expect(repository.raw_repository).to receive(:branch_count).and_return(3)
+
+ expect(subject).to eq(true)
+ end
+
+ it 'caches the output' do
+ expect(repository.raw_repository).to receive(:branch_count).
+ once.
+ and_return(3)
+
+ repository.has_visible_content?
+ repository.has_visible_content?
+ end
+ end
+ end
+
+ describe '#update_autocrlf_option' do
+ describe 'when autocrlf is not already set to :input' do
+ before do
+ repository.raw_repository.autocrlf = true
+ end
+
+ it 'sets autocrlf to :input' do
+ repository.update_autocrlf_option
+
+ expect(repository.raw_repository.autocrlf).to eq(:input)
+ end
+ end
+
+ describe 'when autocrlf is already set to :input' do
+ before do
+ repository.raw_repository.autocrlf = :input
+ end
+
+ it 'does nothing' do
+ expect(repository.raw_repository).to_not receive(:autocrlf=).
+ with(:input)
+
+ repository.update_autocrlf_option
+ end
+ end
+ end
+
+ describe '#empty?' do
+ let(:empty_repository) { create(:project_empty_repo).repository }
+
+ it 'returns true for an empty repository' do
+ expect(empty_repository.empty?).to eq(true)
+ end
+
+ it 'returns false for a non-empty repository' do
+ expect(repository.empty?).to eq(false)
+ end
+
+ it 'caches the output' do
+ expect(repository.raw_repository).to receive(:empty?).
+ once.
+ and_return(false)
+
+ repository.empty?
+ repository.empty?
+ end
+ end
+
+ describe '#root_ref' do
+ it 'returns a branch name' do
+ expect(repository.root_ref).to be_an_instance_of(String)
+ end
+
+ it 'caches the output' do
+ expect(repository.raw_repository).to receive(:root_ref).
+ once.
+ and_return('master')
+
+ repository.root_ref
+ repository.root_ref
+ end
+ end
+
+ describe '#expire_cache' do
+ it 'expires all caches' do
+ expect(repository).to receive(:expire_branch_cache)
+
+ repository.expire_cache
+ end
+
+ it 'expires the caches for a specific branch' do
+ expect(repository).to receive(:expire_branch_cache).with('master')
+
+ repository.expire_cache('master')
+ end
+
+ it 'expires the emptiness caches for an empty repository' do
+ expect(repository).to receive(:empty?).and_return(true)
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.expire_cache
+ end
+
+ it 'does not expire the emptiness caches for a non-empty repository' do
+ expect(repository).to receive(:empty?).and_return(false)
+ expect(repository).to_not receive(:expire_emptiness_caches)
+
+ repository.expire_cache
+ end
+ end
+
+ describe '#expire_root_ref_cache' do
+ it 'expires the root reference cache' do
+ repository.root_ref
+
+ expect(repository.raw_repository).to receive(:root_ref).
+ once.
+ and_return('foo')
+
+ repository.expire_root_ref_cache
+
+ expect(repository.root_ref).to eq('foo')
+ end
+ end
+
+ describe '#expire_has_visible_content_cache' do
+ it 'expires the visible content cache' do
+ repository.has_visible_content?
+
+ expect(repository.raw_repository).to receive(:branch_count).
+ once.
+ and_return(0)
+
+ repository.expire_has_visible_content_cache
+
+ expect(repository.has_visible_content?).to eq(false)
+ end
+ end
+
+ describe '#expire_branch_cache' do
+ # This method is private but we need it for testing purposes. Sadly there's
+ # no other proper way of testing caching operations.
+ let(:cache) { repository.send(:cache) }
+
+ it 'expires the cache for all branches' do
+ expect(cache).to receive(:expire).
+ at_least(repository.branches.length).
+ times
+
+ repository.expire_branch_cache
+ end
+
+ it 'expires the cache for all branches when the root branch is given' do
+ expect(cache).to receive(:expire).
+ at_least(repository.branches.length).
+ times
+
+ repository.expire_branch_cache(repository.root_ref)
+ end
+
+ it 'expires the cache for a specific branch' do
+ expect(cache).to receive(:expire).once
+
+ repository.expire_branch_cache('foo')
+ end
+ end
+
+ describe '#expire_emptiness_caches' do
+ let(:cache) { repository.send(:cache) }
+
+ it 'expires the caches' do
+ expect(cache).to receive(:expire).with(:empty?)
+ expect(repository).to receive(:expire_has_visible_content_cache)
+
+ repository.expire_emptiness_caches
+ end
+ end
+
+ describe :skip_merged_commit do
+ subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", nil, 100, 0, true).map{ |k| k.id } }
+
+ it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') }
+ end
+
+ describe '#merge' do
+ it 'should merge the code and return the commit id' do
+ expect(merge_commit).to be_present
+ expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
+ end
+ end
+
+ describe '#revert' do
+ let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
+ let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+
+ context 'when there is a conflict' do
+ it 'should abort the operation' do
+ expect(repository.revert(user, new_image_commit, 'master')).to eq(false)
+ end
+ end
+
+ context 'when commit was already reverted' do
+ it 'should abort the operation' do
+ repository.revert(user, update_image_commit, 'master')
+
+ expect(repository.revert(user, update_image_commit, 'master')).to eq(false)
+ end
+ end
+
+ context 'when commit can be reverted' do
+ it 'should revert the changes' do
+ expect(repository.revert(user, update_image_commit, 'master')).to be_truthy
+ end
+ end
+
+ context 'reverting a merge commit' do
+ it 'should revert the changes' do
+ merge_commit
+ expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
+
+ repository.revert(user, merge_commit, 'master')
+ expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
+ end
+ end
+ end
+
+ describe '#before_delete' do
+ describe 'when a repository does not exist' do
+ before do
+ allow(repository).to receive(:exists?).and_return(false)
+ end
+
+ it 'does not flush caches that depend on repository data' do
+ expect(repository).to_not receive(:expire_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the root ref cache' do
+ expect(repository).to receive(:expire_root_ref_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the emptiness caches' do
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.before_delete
+ end
+ end
+
+ describe 'when a repository exists' do
+ before do
+ allow(repository).to receive(:exists?).and_return(true)
+ end
+
+ it 'flushes the caches that depend on repository data' do
+ expect(repository).to receive(:expire_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the root ref cache' do
+ expect(repository).to receive(:expire_root_ref_cache)
+
+ repository.before_delete
+ end
+
+ it 'flushes the emptiness caches' do
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.before_delete
+ end
+ end
+ end
+
+ describe '#before_change_head' do
+ it 'flushes the branch cache' do
+ expect(repository).to receive(:expire_branch_cache)
+
+ repository.before_change_head
+ end
+
+ it 'flushes the root ref cache' do
+ expect(repository).to receive(:expire_root_ref_cache)
+
+ repository.before_change_head
+ end
+ end
+
+ describe '#before_push_tag' do
+ it 'flushes the cache' do
+ expect(repository).to receive(:expire_cache)
+ expect(repository).to receive(:expire_tag_count_cache)
+
+ repository.before_push_tag
+ end
+ end
+
+ describe '#after_import' do
+ it 'flushes the emptiness cachess' do
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.after_import
+ end
+ end
+
+ describe '#after_push_commit' do
+ it 'flushes the cache' do
+ expect(repository).to receive(:expire_cache).with('master', '123')
+
+ repository.after_push_commit('master', '123')
+ end
+ end
+
+ describe '#after_create_branch' do
+ it 'flushes the visible content cache' do
+ expect(repository).to receive(:expire_has_visible_content_cache)
+
+ repository.after_create_branch
+ end
+ end
+
+ describe '#after_remove_branch' do
+ it 'flushes the visible content cache' do
+ expect(repository).to receive(:expire_has_visible_content_cache)
+
+ repository.after_remove_branch
+ end
+ end
+
+ describe "#main_language" do
+ it 'shows the main language of the project' do
+ expect(repository.main_language).to eq("Ruby")
+ end
+
+ it 'returns nil when the repository is empty' do
+ allow(repository).to receive(:empty?).and_return(true)
+
+ expect(repository.main_language).to be_nil
+ end
+ end
+
+ describe '#before_remove_tag' do
+ it 'flushes the tag cache' do
+ expect(repository).to receive(:expire_tag_count_cache)
+
+ repository.before_remove_tag
+ end
+ end
+
+ describe '#branch_count' do
+ it 'returns the number of branches' do
+ expect(repository.branch_count).to be_an_instance_of(Fixnum)
+ end
+ end
+
+ describe '#tag_count' do
+ it 'returns the number of tags' do
+ expect(repository.tag_count).to be_an_instance_of(Fixnum)
+ end
+ end
+
+ describe '#expire_branch_count_cache' do
+ let(:cache) { repository.send(:cache) }
+
+ it 'expires the cache' do
+ expect(cache).to receive(:expire).with(:branch_count)
+
+ repository.expire_branch_count_cache
+ end
+ end
+
+ describe '#expire_tag_count_cache' do
+ let(:cache) { repository.send(:cache) }
+
+ it 'expires the cache' do
+ expect(cache).to receive(:expire).with(:tag_count)
+
+ repository.expire_tag_count_cache
+ end
+ end
+
+ describe '#add_tag' do
+ it 'adds a tag' do
+ expect(repository).to receive(:before_push_tag)
+
+ expect_any_instance_of(Gitlab::Shell).to receive(:add_tag).
+ with(repository.path_with_namespace, '8.5', 'master', 'foo')
+
+ repository.add_tag('8.5', 'master', 'foo')
+ end
+ end
+
+ describe '#rm_branch' do
+ let(:user) { create(:user) }
+
+ it 'removes a branch' do
+ expect(repository).to receive(:before_remove_branch)
+ expect(repository).to receive(:after_remove_branch)
+
+ repository.rm_branch(user, 'feature')
+ end
+ end
+
+ describe '#rm_tag' do
+ it 'removes a tag' do
+ expect(repository).to receive(:before_remove_tag)
+
+ expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
+ with(repository.path_with_namespace, '8.5')
+
+ repository.rm_tag('8.5')
+ end
+ end
+
+ describe '#avatar' do
+ it 'returns the first avatar file found in the repository' do
+ expect(repository).to receive(:blob_at_branch).
+ with('master', 'logo.png').
+ and_return(true)
+
+ expect(repository.avatar).to eq('logo.png')
+ end
+
+ it 'caches the output' do
+ allow(repository).to receive(:blob_at_branch).
+ with('master', 'logo.png').
+ and_return(true)
+
+ expect(repository.avatar).to eq('logo.png')
+
+ expect(repository).to_not receive(:blob_at_branch)
+ expect(repository.avatar).to eq('logo.png')
+ end
+ end
+
+ describe '#expire_avatar_cache' do
+ let(:cache) { repository.send(:cache) }
+
+ before do
+ allow(repository).to receive(:cache).and_return(cache)
+ end
+
+ context 'without a branch or revision' do
+ it 'flushes the cache' do
+ expect(cache).to receive(:expire).with(:avatar)
+
+ repository.expire_avatar_cache
+ end
+ end
+
+ context 'with a branch' do
+ it 'does not flush the cache if the branch is not the default branch' do
+ expect(cache).not_to receive(:expire)
+
+ repository.expire_avatar_cache('cats')
+ end
+
+ it 'flushes the cache if the branch equals the default branch' do
+ expect(cache).to receive(:expire).with(:avatar)
+
+ repository.expire_avatar_cache(repository.root_ref)
+ end
+ end
+
+ context 'with a branch and revision' do
+ let(:commit) { double(:commit) }
+
+ before do
+ allow(repository).to receive(:commit).and_return(commit)
+ end
+
+ it 'does not flush the cache if the commit does not change any logos' do
+ diff = double(:diff, new_path: 'test.txt')
+
+ expect(commit).to receive(:diffs).and_return([diff])
+ expect(cache).not_to receive(:expire)
+
+ repository.expire_avatar_cache(repository.root_ref, '123')
+ end
+
+ it 'flushes the cache if the commit changes any of the logos' do
+ diff = double(:diff, new_path: Repository::AVATAR_FILES[0])
+
+ expect(commit).to receive(:diffs).and_return([diff])
+ expect(cache).to receive(:expire).with(:avatar)
+
+ repository.expire_avatar_cache(repository.root_ref, '123')
+ end
+ 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/service_spec.rb b/spec/models/service_spec.rb
index 0ca82365b98..173628c08d0 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require 'spec_helper'
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index eb2dbbdc5a4..5077ac7b62b 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
@@ -60,4 +59,48 @@ describe Snippet, models: true do
expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}"
end
end
+
+ describe '.search' do
+ let(:snippet) { create(:snippet) }
+
+ it 'returns snippets with a matching title' do
+ expect(described_class.search(snippet.title)).to eq([snippet])
+ end
+
+ it 'returns snippets with a partially matching title' do
+ expect(described_class.search(snippet.title[0..2])).to eq([snippet])
+ end
+
+ it 'returns snippets with a matching title regardless of the casing' do
+ expect(described_class.search(snippet.title.upcase)).to eq([snippet])
+ end
+
+ it 'returns snippets with a matching file name' do
+ expect(described_class.search(snippet.file_name)).to eq([snippet])
+ end
+
+ it 'returns snippets with a partially matching file name' do
+ expect(described_class.search(snippet.file_name[0..2])).to eq([snippet])
+ end
+
+ it 'returns snippets with a matching file name regardless of the casing' do
+ expect(described_class.search(snippet.file_name.upcase)).to eq([snippet])
+ end
+ end
+
+ describe '#search_code' do
+ let(:snippet) { create(:snippet, content: 'class Foo; end') }
+
+ it 'returns snippets with matching content' do
+ expect(described_class.search_code(snippet.content)).to eq([snippet])
+ end
+
+ it 'returns snippets with partially matching content' do
+ expect(described_class.search_code('class')).to eq([snippet])
+ end
+
+ it 'returns snippets with matching content regardless of the casing' do
+ expect(described_class.search_code('FOO')).to eq([snippet])
+ end
+ end
end
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
new file mode 100644
index 00000000000..c4ec7625cb0
--- /dev/null
+++ b/spec/models/spam_log_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe SpamLog, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+ end
+
+ describe '#remove_user' do
+ it 'blocks the user' do
+ spam_log = build(:spam_log)
+
+ expect { spam_log.remove_user }.to change { spam_log.user.blocked? }.to(true)
+ end
+
+ it 'removes the user' do
+ spam_log = build(:spam_log)
+
+ expect { spam_log.remove_user }.to change { User.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
new file mode 100644
index 00000000000..fe9ea7e7d1e
--- /dev/null
+++ b/spec/models/todo_spec.rb
@@ -0,0 +1,69 @@
+# == Schema Information
+#
+# Table name: todos
+#
+# id :integer not null, primary key
+# user_id :integer not null
+# project_id :integer not null
+# target_id :integer not null
+# 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
+#
+
+require 'spec_helper'
+
+describe Todo, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:author).class_name("User") }
+ it { is_expected.to belong_to(:note) }
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:target).touch(true) }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'respond to' do
+ it { is_expected.to respond_to(:author_name) }
+ it { is_expected.to respond_to(:author_email) }
+ end
+
+ 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(:user) }
+ end
+
+ describe '#body' do
+ before do
+ subject.target = build(:issue, title: 'Bugfix')
+ end
+
+ it 'returns target title when note is blank' do
+ subject.note = nil
+
+ expect(subject.body).to eq 'Bugfix'
+ end
+
+ it 'returns note when note is present' do
+ subject.note = build(:note, note: 'quick fix')
+
+ expect(subject.body).to eq 'quick fix'
+ end
+ end
+
+ 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')
+ end
+
+ it 'does not raise error when is already done' do
+ todo = create(:todo, state: :done)
+ expect { todo.done! }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb
new file mode 100644
index 00000000000..0737999e125
--- /dev/null
+++ b/spec/models/tree_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Tree, models: true do
+ let(:repository) { create(:project).repository }
+ let(:sha) { repository.root_ref }
+
+ subject { described_class.new(repository, '54fcc214') }
+
+ describe '#readme' do
+ class FakeBlob
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def readme?
+ name =~ /^readme/i
+ end
+ end
+
+ it 'returns nil when repository does not contains a README file' do
+ files = [FakeBlob.new('file'), FakeBlob.new('license'), FakeBlob.new('copying')]
+ expect(subject).to receive(:blobs).and_return(files)
+
+ expect(subject.readme).to eq nil
+ end
+
+ it 'returns nil when repository does not contains a previewable README file' do
+ files = [FakeBlob.new('file'), FakeBlob.new('README.pages'), FakeBlob.new('README.png')]
+ expect(subject).to receive(:blobs).and_return(files)
+
+ expect(subject.readme).to eq nil
+ end
+
+ it 'returns README when repository contains a previewable README file' do
+ files = [FakeBlob.new('README.png'), FakeBlob.new('README'), FakeBlob.new('file')]
+ expect(subject).to receive(:blobs).and_return(files)
+
+ expect(subject.readme.name).to eq 'README'
+ end
+
+ it 'returns first previewable README when repository contains more than one' do
+ files = [FakeBlob.new('file'), FakeBlob.new('README.md'), FakeBlob.new('README.asciidoc')]
+ expect(subject).to receive(:blobs).and_return(files)
+
+ expect(subject.readme.name).to eq 'README.md'
+ end
+
+ it 'returns first plain text README when repository contains more than one' do
+ files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.txt')]
+ expect(subject).to receive(:blobs).and_return(files)
+
+ expect(subject.readme.name).to eq 'README'
+ end
+
+ it 'prioritizes previewable README file over one in plain text' do
+ files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.md')]
+ expect(subject).to receive(:blobs).and_return(files)
+
+ expect(subject.readme.name).to eq 'README.md'
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 2f184bbaf92..0ab7fd88ce6 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,62 +2,63 @@
#
# Table name: users
#
-# id :integer not null, primary key
-# email :string(255) default(""), not null
-# encrypted_password :string(255) default(""), not null
-# reset_password_token :string(255)
-# reset_password_sent_at :datetime
-# remember_created_at :datetime
-# sign_in_count :integer default(0)
-# current_sign_in_at :datetime
-# last_sign_in_at :datetime
-# current_sign_in_ip :string(255)
-# last_sign_in_ip :string(255)
-# created_at :datetime
-# updated_at :datetime
-# name :string(255)
-# admin :boolean default(FALSE), not null
-# projects_limit :integer default(10)
-# skype :string(255) default(""), not null
-# linkedin :string(255) default(""), not null
-# twitter :string(255) default(""), not null
-# authentication_token :string(255)
-# theme_id :integer default(1), not null
-# bio :string(255)
-# failed_attempts :integer default(0)
-# locked_at :datetime
-# unlock_token :string(255)
-# username :string(255)
-# can_create_group :boolean default(TRUE), not null
-# can_create_team :boolean default(TRUE), not null
-# state :string(255)
-# color_scheme_id :integer default(1), not null
-# notification_level :integer default(1), not null
-# password_expires_at :datetime
-# created_by_id :integer
-# last_credential_check_at :datetime
-# avatar :string(255)
-# confirmation_token :string(255)
-# confirmed_at :datetime
-# confirmation_sent_at :datetime
-# unconfirmed_email :string(255)
-# hide_no_ssh_key :boolean default(FALSE)
-# website_url :string(255) default(""), not null
-# notification_email :string(255)
-# hide_no_password :boolean default(FALSE)
-# password_automatically_set :boolean default(FALSE)
-# location :string(255)
-# encrypted_otp_secret :string(255)
-# encrypted_otp_secret_iv :string(255)
-# encrypted_otp_secret_salt :string(255)
-# otp_required_for_login :boolean default(FALSE), not null
-# otp_backup_codes :text
-# public_email :string(255) default(""), not null
-# dashboard :integer default(0)
-# project_view :integer default(0)
-# consumed_timestep :integer
-# layout :integer default(0)
-# hide_project_limit :boolean default(FALSE)
+# id :integer not null, primary key
+# email :string(255) default(""), not null
+# encrypted_password :string(255) default(""), not null
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime
+# updated_at :datetime
+# name :string(255)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
+# skype :string(255) default(""), not null
+# linkedin :string(255) default(""), not null
+# twitter :string(255) default(""), not null
+# authentication_token :string(255)
+# theme_id :integer default(1), not null
+# bio :string(255)
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# username :string(255)
+# can_create_group :boolean default(TRUE), not null
+# can_create_team :boolean default(TRUE), not null
+# state :string(255)
+# color_scheme_id :integer default(1), not null
+# notification_level :integer default(1), not null
+# password_expires_at :datetime
+# created_by_id :integer
+# last_credential_check_at :datetime
+# avatar :string(255)
+# confirmation_token :string(255)
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string(255)
+# hide_no_ssh_key :boolean default(FALSE)
+# website_url :string(255) default(""), not null
+# notification_email :string(255)
+# hide_no_password :boolean default(FALSE)
+# password_automatically_set :boolean default(FALSE)
+# location :string(255)
+# encrypted_otp_secret :string(255)
+# encrypted_otp_secret_iv :string(255)
+# encrypted_otp_secret_salt :string(255)
+# otp_required_for_login :boolean default(FALSE), not null
+# otp_backup_codes :text
+# public_email :string(255) default(""), not null
+# dashboard :integer default(0)
+# project_view :integer default(0)
+# consumed_timestep :integer
+# layout :integer default(0)
+# hide_project_limit :boolean default(FALSE)
+# unlock_token :string
+# otp_grace_period_started_at :datetime
#
require 'spec_helper'
@@ -90,6 +91,8 @@ describe User, models: true do
it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
it { is_expected.to have_one(:abuse_report) }
+ it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
+ it { is_expected.to have_many(:todos).dependent(:destroy) }
end
describe 'validations' do
@@ -106,7 +109,7 @@ describe User, models: true do
end
it 'validates uniqueness' do
- expect(subject).to validate_uniqueness_of(:username)
+ expect(subject).to validate_uniqueness_of(:username).case_insensitive
end
end
@@ -117,37 +120,15 @@ describe User, models: true do
it { is_expected.to validate_length_of(:bio).is_within(0..255) }
- describe 'email' do
- it 'accepts info@example.com' do
- user = build(:user, email: 'info@example.com')
- expect(user).to be_valid
- end
-
- it 'accepts info+test@example.com' do
- user = build(:user, email: 'info+test@example.com')
- expect(user).to be_valid
- end
-
- it "accepts o'reilly@example.com" do
- user = build(:user, email: "o'reilly@example.com")
- expect(user).to be_valid
- end
-
- it 'rejects test@test@example.com' do
- user = build(:user, email: 'test@test@example.com')
- expect(user).to be_invalid
- end
-
- it 'rejects mailto:test@example.com' do
- user = build(:user, email: 'mailto:test@example.com')
- expect(user).to be_invalid
- end
+ it_behaves_like 'an object with email-formated attributes', :email do
+ subject { build(:user) }
+ end
- it "rejects lol!'+=?><#$%^&*()@gmail.com" do
- user = build(:user, email: "lol!'+=?><#$%^&*()@gmail.com")
- expect(user).to be_invalid
- end
+ it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do
+ subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } }
+ end
+ describe 'email' do
context 'when no signup domains listed' do
before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) }
it 'accepts any email' do
@@ -199,6 +180,20 @@ describe User, models: true do
it { is_expected.to respond_to(:is_admin?) }
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:private_token) }
+ it { is_expected.to respond_to(:external?) }
+ end
+
+ describe 'before save hook' do
+ context 'when saving an external user' do
+ let(:user) { create(:user) }
+ let(:external_user) { create(:user, external: true) }
+
+ it "sets other properties aswell" do
+ expect(external_user.can_create_team).to be_falsey
+ expect(external_user.can_create_group).to be_falsey
+ expect(external_user.projects_limit).to be 0
+ end
+ end
end
describe '#confirm' do
@@ -275,6 +270,7 @@ describe User, models: true do
expect(user).to be_two_factor_enabled
expect(user.encrypted_otp_secret).not_to be_nil
expect(user.otp_backup_codes).not_to be_nil
+ expect(user.otp_grace_period_started_at).not_to be_nil
user.disable_two_factor!
@@ -283,6 +279,7 @@ describe User, models: true do
expect(user.encrypted_otp_secret_iv).to be_nil
expect(user.encrypted_otp_secret_salt).to be_nil
expect(user.otp_backup_codes).to be_nil
+ expect(user.otp_grace_period_started_at).to be_nil
end
end
@@ -421,6 +418,7 @@ describe User, models: true do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
+ expect(user.external).to be_falsey
end
end
@@ -454,17 +452,43 @@ describe User, models: true do
end
end
- describe 'search' do
- let(:user1) { create(:user, username: 'James', email: 'james@testing.com') }
- let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') }
+ describe '.search' do
+ let(:user) { create(:user) }
- it "should be case insensitive" do
- expect(User.search(user1.username.upcase).to_a).to eq([user1])
- expect(User.search(user1.username.downcase).to_a).to eq([user1])
- expect(User.search(user2.username.upcase).to_a).to eq([user2])
- expect(User.search(user2.username.downcase).to_a).to eq([user2])
- expect(User.search(user1.username.downcase).to_a.size).to eq(2)
- expect(User.search(user2.username.downcase).to_a.size).to eq(1)
+ it 'returns users with a matching name' do
+ expect(described_class.search(user.name)).to eq([user])
+ end
+
+ it 'returns users with a partially matching name' do
+ expect(described_class.search(user.name[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching name regardless of the casing' do
+ expect(described_class.search(user.name.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching Email' do
+ expect(described_class.search(user.email)).to eq([user])
+ end
+
+ it 'returns users with a partially matching Email' do
+ expect(described_class.search(user.email[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching Email regardless of the casing' do
+ expect(described_class.search(user.email.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching username' do
+ expect(described_class.search(user.username)).to eq([user])
+ end
+
+ it 'returns users with a partially matching username' do
+ expect(described_class.search(user.username[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching username regardless of the casing' do
+ expect(described_class.search(user.username.upcase)).to eq([user])
end
end
@@ -568,27 +592,39 @@ describe User, models: true do
end
end
- describe :ldap_user? do
- it "is true if provider name starts with ldap" do
- user = create(:omniauth_user, provider: 'ldapmain')
- expect( user.ldap_user? ).to be_truthy
- end
+ context 'ldap synchronized user' do
+ describe :ldap_user? do
+ it 'is true if provider name starts with ldap' do
+ user = create(:omniauth_user, provider: 'ldapmain')
+ expect(user.ldap_user?).to be_truthy
+ end
+
+ it 'is false for other providers' do
+ user = create(:omniauth_user, provider: 'other-provider')
+ expect(user.ldap_user?).to be_falsey
+ end
- it "is false for other providers" do
- user = create(:omniauth_user, provider: 'other-provider')
- expect( user.ldap_user? ).to be_falsey
+ it 'is false if no extern_uid is provided' do
+ user = create(:omniauth_user, extern_uid: nil)
+ expect(user.ldap_user?).to be_falsey
+ end
end
- it "is false if no extern_uid is provided" do
- user = create(:omniauth_user, extern_uid: nil)
- expect( user.ldap_user? ).to be_falsey
+ describe :ldap_identity do
+ it 'returns ldap identity' do
+ user = create :omniauth_user
+ expect(user.ldap_identity.provider).not_to be_empty
+ end
end
- end
- describe :ldap_identity do
- it "returns ldap identity" do
- user = create :omniauth_user
- expect(user.ldap_identity.provider).not_to be_empty
+ describe '#ldap_block' do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain', name: 'John Smith') }
+
+ it 'blocks user flaging the action caming from ldap' do
+ user.ldap_block
+ expect(user.blocked?).to be_truthy
+ expect(user.ldap_blocked?).to be_truthy
+ end
end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index c1b03838aa9..ddc49495eda 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -189,6 +189,38 @@ describe WikiPage, models: true do
end
end
+ describe '#historical?' do
+ before do
+ create_page('Update', 'content')
+ @page = wiki.find_page('Update')
+ 3.times { |i| @page.update("content #{i}") }
+ end
+
+ after do
+ destroy_page('Update')
+ end
+
+ it 'returns true when requesting an old version' do
+ old_version = @page.versions.last.to_s
+ old_page = wiki.find_page('Update', old_version)
+
+ expect(old_page.historical?).to eq true
+ end
+
+ it 'returns false when requesting latest version' do
+ latest_version = @page.versions.first.to_s
+ latest_page = wiki.find_page('Update', latest_version)
+
+ expect(latest_page.historical?).to eq false
+ end
+
+ it 'returns false when version is nil' do
+ latest_page = wiki.find_page('Update', nil)
+
+ expect(latest_page.historical?).to eq false
+ end
+ end
+
private
def remove_temp_repo(path)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 36461e84c3a..55582aa53d2 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -7,8 +7,8 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
- let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
- let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
new file mode 100644
index 00000000000..967c34800d0
--- /dev/null
+++ b/spec/requests/api/builds_spec.rb
@@ -0,0 +1,244 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:api_user) { user }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:developer) { create(:project_member, :developer, user: user, project: project) }
+ let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) }
+ let(:commit) { create(:ci_commit, project: project)}
+ let(:build) { create(:ci_build, commit: commit) }
+
+ describe 'GET /projects/:id/builds ' do
+ let(:query) { '' }
+
+ before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
+
+ context 'authorized user' do
+ it 'should return project builds' do
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+
+ context 'filter project with one scope element' do
+ let(:query) { 'scope=pending' }
+
+ it do
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'filter project with array of scope elements' do
+ let(:query) { 'scope[0]=pending&scope[1]=running' }
+
+ it do
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { 'scope[0]=pending&scope[1]=unknown_status' }
+
+ it { expect(response.status).to eq(400) }
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'should not return project builds' do
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/commits/:sha/builds' do
+ before do
+ project.ensure_ci_commit(commit.sha)
+ get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user)
+ end
+
+ context 'authorized user' do
+ it 'should return project builds for specific commit' do
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'should not return project builds' do
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id' do
+ before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
+
+ context 'authorized user' do
+ it 'should return specific build data' do
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq('test')
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'should not return specific build data' do
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id/artifacts' do
+ before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) }
+
+ context 'build with artifacts' do
+ let(:build) { create(:ci_build, :artifacts, commit: commit) }
+
+ context 'authorized user' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding'=>'binary',
+ 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ it 'should return specific build artifacts' do
+ expect(response.status).to eq(200)
+ expect(response.headers).to include(download_headers)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'should not return specific build artifacts' do
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ it 'should not return build artifacts if not uploaded' do
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id/trace' do
+ let(:build) { create(:ci_build, :trace, commit: commit) }
+
+ before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) }
+
+ context 'authorized user' do
+ it 'should return specific build trace' do
+ expect(response.status).to eq(200)
+ expect(response.body).to eq(build.trace)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'should not return specific build trace' do
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/cancel' do
+ before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) }
+
+ context 'authorized user' do
+ context 'user with :update_build persmission' do
+ it 'should cancel running or pending build' do
+ expect(response.status).to eq(201)
+ expect(project.builds.first.status).to eq('canceled')
+ end
+ end
+
+ context 'user without :update_build permission' do
+ let(:api_user) { user2 }
+
+ it 'should not cancel build' do
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'should not cancel build' do
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/retry' do
+ let(:build) { create(:ci_build, :canceled, commit: commit) }
+
+ before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) }
+
+ context 'authorized user' do
+ context 'user with :update_build permission' do
+ it 'should retry non-running build' do
+ expect(response.status).to eq(201)
+ expect(project.builds.first.status).to eq('canceled')
+ expect(json_response['status']).to eq('pending')
+ end
+ end
+
+ context 'user without :update_build permission' do
+ let(:api_user) { user2 }
+
+ it 'should not retry build' do
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'should not retry build' do
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/erase' do
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/erase", user)
+ end
+
+ context 'build is erasable' do
+ let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, commit: commit) }
+
+ it 'should erase build content' do
+ expect(response.status).to eq 201
+ expect(build.trace).to be_empty
+ expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ end
+
+ it 'should update build' do
+ expect(build.reload.erased_at).to be_truthy
+ expect(build.reload.erased_by).to eq user
+ end
+ end
+
+ context 'build is not erasable' do
+ let(:build) { create(:ci_build, :trace, project: project, commit: commit) }
+
+ it 'should respond with forbidden' do
+ expect(response.status).to eq 403
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb
index a28607bd240..429a24109fd 100644
--- a/spec/requests/api/commit_status_spec.rb
+++ b/spec/requests/api/commit_status_spec.rb
@@ -1,86 +1,126 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::CommitStatus, api: true do
include ApiHelpers
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id) }
- let!(:reporter) { create(:project_member, user: user, project: project, access_level: ProjectMember::REPORTER) }
- let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+
+ let!(:project) { create(:project) }
let(:commit) { project.repository.commit }
- let!(:ci_commit) { project.ensure_ci_commit(commit.id) }
let(:commit_status) { create(:commit_status, commit: ci_commit) }
+ let(:guest) { create_user(:guest) }
+ let(:reporter) { create_user(:reporter) }
+ let(:developer) { create_user(:developer) }
+ let(:sha) { commit.id }
+
describe "GET /projects/:id/repository/commits/:sha/statuses" do
- context "reporter user" do
- let(:statuses_id) { json_response.map { |status| status['id'] } }
-
- before do
- @status1 = create(:commit_status, commit: ci_commit, status: 'running')
- @status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending')
- @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running', allow_failure: true)
- @status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success')
- @status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success')
- @status6 = create(:commit_status, commit: ci_commit, status: 'success')
- end
+ let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
- it "should return latest commit statuses" do
- get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user)
- expect(response.status).to eq(200)
+ context 'ci commit exists' do
+ let!(:ci_commit) { project.ensure_ci_commit(commit.id) }
- expect(json_response).to be_an Array
- expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id)
- json_response.sort_by!{ |status| status['id'] }
- expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false])
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api(get_url, reporter) }
end
- it "should return all commit statuses" do
- get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", user)
- expect(response.status).to eq(200)
+ context "reporter user" do
+ let(:statuses_id) { json_response.map { |status| status['id'] } }
- expect(json_response).to be_an Array
- expect(statuses_id).to contain_exactly(@status1.id, @status2.id, @status3.id, @status4.id, @status5.id, @status6.id)
- end
+ def create_status(opts = {})
+ create(:commit_status, { commit: ci_commit }.merge(opts))
+ end
- it "should return latest commit statuses for specific ref" do
- get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", user)
- expect(response.status).to eq(200)
+ let!(:status1) { create_status(status: 'running') }
+ let!(:status2) { create_status(name: 'coverage', status: 'pending') }
+ let!(:status3) { create_status(ref: 'develop', status: 'running', allow_failure: true) }
+ let!(:status4) { create_status(name: 'coverage', status: 'success') }
+ let!(:status5) { create_status(name: 'coverage', ref: 'develop', status: 'success') }
+ let!(:status6) { create_status(status: 'success') }
- expect(json_response).to be_an Array
- expect(statuses_id).to contain_exactly(@status3.id, @status5.id)
+ context 'latest commit statuses' do
+ before { get api(get_url, reporter) }
+
+ it 'returns latest commit statuses' do
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id)
+ json_response.sort_by!{ |status| status['id'] }
+ expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false])
+ end
+ end
+
+ context 'all commit statuses' do
+ before { get api(get_url, reporter), all: 1 }
+
+ it 'returns all commit statuses' do
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(statuses_id).to contain_exactly(status1.id, status2.id,
+ status3.id, status4.id,
+ status5.id, status6.id)
+ end
+ end
+
+ context 'latest commit statuses for specific ref' do
+ before { get api(get_url, reporter), ref: 'develop' }
+
+ it 'returns latest commit statuses for specific ref' do
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(statuses_id).to contain_exactly(status3.id, status5.id)
+ end
+ end
+
+ context 'latest commit statues for specific name' do
+ before { get api(get_url, reporter), name: 'coverage' }
+
+ it 'return latest commit statuses for specific name' do
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(statuses_id).to contain_exactly(status4.id, status5.id)
+ end
+ end
end
+ end
- it "should return latest commit statuses for specific name" do
- get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", user)
- expect(response.status).to eq(200)
+ context 'ci commit does not exist' do
+ before { get api(get_url, reporter) }
+ it 'returns empty array' do
+ expect(response.status).to eq 200
expect(json_response).to be_an Array
- expect(statuses_id).to contain_exactly(@status3.id, @status4.id)
+ expect(json_response).to be_empty
end
end
context "guest user" do
+ before { get api(get_url, guest) }
+
it "should not return project commits" do
- get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user2)
expect(response.status).to eq(403)
end
end
context "unauthorized user" do
+ before { get api(get_url) }
+
it "should not return project commits" do
- get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses")
expect(response.status).to eq(401)
end
end
end
describe 'POST /projects/:id/statuses/:sha' do
- let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" }
+ let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" }
- context 'reporter user' do
- context 'should create commit status' do
- it 'with only required parameters' do
- post api(post_url, user), state: 'success'
+ context 'developer user' do
+ context 'only required parameters' do
+ before { post api(post_url, developer), state: 'success' }
+
+ it 'creates commit status' do
expect(response.status).to eq(201)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
@@ -89,9 +129,17 @@ describe API::API, api: true do
expect(json_response['target_url']).to be_nil
expect(json_response['description']).to be_nil
end
+ end
+
+ context 'with all optional parameters' do
+ before do
+ optional_params = { state: 'success', context: 'coverage',
+ ref: 'develop', target_url: 'url', description: 'test' }
+
+ post api(post_url, developer), optional_params
+ end
- it 'with all optional parameters' do
- post api(post_url, user), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test'
+ it 'creates commit status' do
expect(response.status).to eq(201)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
@@ -102,36 +150,60 @@ describe API::API, api: true do
end
end
- context 'should not create commit status' do
- it 'with invalid state' do
- post api(post_url, user), state: 'invalid'
+ context 'invalid status' do
+ before { post api(post_url, developer), state: 'invalid' }
+
+ it 'does not create commit status' do
expect(response.status).to eq(400)
end
+ end
+
+ context 'request without state' do
+ before { post api(post_url, developer) }
- it 'without state' do
- post api(post_url, user)
+ it 'does not create commit status' do
expect(response.status).to eq(400)
end
+ end
- it 'invalid commit' do
- post api("/projects/#{project.id}/statuses/invalid_sha", user), state: 'running'
+ context 'invalid commit' do
+ let(:sha) { 'invalid_sha' }
+ before { post api(post_url, developer), state: 'running' }
+
+ it 'returns not found error' do
expect(response.status).to eq(404)
end
end
end
+ context 'reporter user' do
+ before { post api(post_url, reporter) }
+
+ it 'should not create commit status' do
+ expect(response.status).to eq(403)
+ end
+ end
+
context 'guest user' do
+ before { post api(post_url, guest) }
+
it 'should not create commit status' do
- post api(post_url, user2)
expect(response.status).to eq(403)
end
end
context 'unauthorized user' do
+ before { post api(post_url) }
+
it 'should not create commit status' do
- post api(post_url)
expect(response.status).to eq(401)
end
end
end
+
+ def create_user(access_level_trait)
+ user = create(:user)
+ create(:project_member, access_level_trait, user: user, project: project)
+ user
+ end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 49acc3368f4..7ff21175c1b 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -6,8 +6,8 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
- let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
- let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') }
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index 3fe7efff5ba..fa94e03ec32 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -12,7 +12,7 @@ describe API::API, api: true do
end
let(:project_user2) do
- create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST)
+ create(:project_member, :guest, user: user2, project: project)
end
describe 'POST /projects/fork/:id' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 8d0ae1475c2..22802dd0e05 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -54,6 +54,18 @@ describe API::API, api: true do
project.team << [user, :developer]
end
+ context "git push with project.wiki" do
+ it 'responds with success' do
+ project_wiki = create(:project, name: 'my.wiki', path: 'my.wiki')
+ project_wiki.team << [user, :developer]
+
+ push(key, project_wiki)
+
+ expect(response.status).to eq(200)
+ expect(json_response["status"]).to be_truthy
+ end
+ end
+
context "git pull" do
it do
pull(key, project)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 5e65ad18c0e..bb2ab058003 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -3,7 +3,11 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
+ let(:non_member) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:assignee) }
+ let(:admin) { create(: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,
@@ -46,10 +57,10 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it "should add pagination headers" do
- get api("/issues?per_page=3", user)
+ it "should add pagination headers and keep query params" do
+ get api("/issues?state=closed&per_page=3", user)
expect(response.headers['Link']).to eq(
- '<http://www.example.com/api/v3/issues?page=1&per_page=3>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3>; rel="last"'
+ '<http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="last"' % [user.private_token, user.private_token]
)
end
@@ -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
@@ -241,6 +320,37 @@ describe API::API, api: true do
end
end
+ describe 'POST /projects/:id/issues with spam filtering' do
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ allow(endpoint).to receive(:check_for_spam?).and_return(true)
+ allow(endpoint).to receive(:is_spam?).and_return(true)
+ end
+ end
+
+ let(:params) do
+ {
+ title: 'new issue',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it "should not create a new project issue" do
+ expect { post api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('new issue')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ expect(spam_logs[0].project_id).to eq(project.id)
+ end
+ end
+
describe "PUT /projects/:id/issues/:issue_id to update only title" do
it "should update a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
@@ -263,6 +373,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
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index e194eb93cf4..4fd1df25568 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -10,6 +10,7 @@ describe API::API, api: true do
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) }
before do
project.team << [user, :reporters]
@@ -109,12 +110,13 @@ describe API::API, api: true do
end
end
- describe "GET /projects/:id/merge_request/:merge_request_id" do
+ describe "GET /projects/:id/merge_requests/:merge_request_id" do
it "should return merge_request" do
- get api("/projects/#{project.id}/merge_request/#{merge_request.id}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
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['merge_status']).to eq('can_be_merged')
end
it 'should return merge_request by iid' do
@@ -126,14 +128,14 @@ describe API::API, api: true do
end
it "should return a 404 error if merge_request_id not found" do
- get api("/projects/#{project.id}/merge_request/999", user)
+ get api("/projects/#{project.id}/merge_requests/999", user)
expect(response.status).to eq(404)
end
end
- describe 'GET /projects/:id/merge_request/:merge_request_id/commits' do
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
context 'valid merge request' do
- before { get api("/projects/#{project.id}/merge_request/#{merge_request.id}/commits", user) }
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) }
let(:commit) { merge_request.commits.first }
it { expect(response.status).to eq 200 }
@@ -143,20 +145,20 @@ describe API::API, api: true do
end
it 'returns a 404 when merge_request_id not found' do
- get api("/projects/#{project.id}/merge_request/999/commits", user)
+ get api("/projects/#{project.id}/merge_requests/999/commits", user)
expect(response.status).to eq(404)
end
end
- describe 'GET /projects/:id/merge_request/:merge_request_id/changes' do
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
it 'should return the change information of the merge_request' do
- get api("/projects/#{project.id}/merge_request/#{merge_request.id}/changes", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
expect(response.status).to eq 200
expect(json_response['changes'].size).to eq(merge_request.diffs.size)
end
it 'returns a 404 when merge_request_id not found' do
- get api("/projects/#{project.id}/merge_request/999/changes", user)
+ get api("/projects/#{project.id}/merge_requests/999/changes", user)
expect(response.status).to eq(404)
end
end
@@ -169,10 +171,12 @@ describe API::API, api: true do
source_branch: 'feature_conflict',
target_branch: 'master',
author: user,
- labels: 'label, label2'
+ labels: 'label, label2',
+ milestone_id: milestone.id
expect(response.status).to eq(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['milestone']['id']).to eq(milestone.id)
end
it "should return 422 when source_branch equals target_branch" do
@@ -311,19 +315,19 @@ describe API::API, api: true do
end
end
- describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do
+ 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_request/#{merge_request.id}", user), state_event: "close"
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
expect(response.status).to eq(200)
expect(json_response['state']).to eq('closed')
end
end
- describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do
+ describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
let(:ci_commit) { create(:ci_commit_without_jobs) }
it "should return merge_request in case of success" do
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response.status).to eq(200)
end
@@ -332,7 +336,7 @@ describe API::API, api: true do
allow_any_instance_of(MergeRequest).
to receive(:can_be_merged?).and_return(false)
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response.status).to eq(406)
expect(json_response['message']).to eq('Branch cannot be merged')
@@ -340,14 +344,14 @@ describe API::API, api: true do
it "should return 405 if merge_request is not open" do
merge_request.close
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response.status).to eq(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
it "should return 405 if merge_request is a work in progress" do
merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response.status).to eq(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
@@ -355,7 +359,7 @@ describe API::API, api: true do
it "should return 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user2)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
expect(response.status).to eq(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
@@ -364,7 +368,7 @@ describe API::API, api: true do
allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
allow(ci_commit).to receive(:active?).and_return(true)
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
expect(response.status).to eq(200)
expect(json_response['title']).to eq('Test')
@@ -372,33 +376,39 @@ describe API::API, api: true do
end
end
- describe "PUT /projects/:id/merge_request/:merge_request_id" do
- it "should return merge_request" do
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), title: "New title"
+ describe "PUT /projects/:id/merge_requests/:merge_request_id" do
+ it "updates title and returns merge_request" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
expect(response.status).to eq(200)
expect(json_response['title']).to eq('New title')
end
- it "should return merge_request" do
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), description: "New description"
+ it "updates description and returns merge_request" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
expect(response.status).to eq(200)
expect(json_response['description']).to eq('New description')
end
+ it "updates milestone_id and returns merge_request" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
+ expect(response.status).to eq(200)
+ expect(json_response['milestone']['id']).to eq(milestone.id)
+ end
+
it "should return 400 when source_branch is specified" do
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user),
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
source_branch: "master", target_branch: "master"
expect(response.status).to eq(400)
end
it "should return merge_request with renamed target_branch" do
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "wiki"
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
expect(response.status).to eq(200)
expect(json_response['target_branch']).to eq('wiki')
end
it 'should return 400 on invalid label names' do
- put api("/projects/#{project.id}/merge_request/#{merge_request.id}",
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}",
user),
title: 'new issue',
labels: 'label, ?'
@@ -407,11 +417,11 @@ describe API::API, api: true do
end
end
- describe "POST /projects/:id/merge_request/:merge_request_id/comments" do
+ describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
it "should return comment" do
original_count = merge_request.notes.size
- post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user), note: "My comment"
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
expect(response.status).to eq(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['author']['name']).to eq(user.name)
@@ -420,20 +430,20 @@ describe API::API, api: true do
end
it "should return 400 if note is missing" do
- post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response.status).to eq(400)
end
it "should return 404 if note is attached to non existent merge request" do
- post api("/projects/#{project.id}/merge_request/404/comments", user),
+ post api("/projects/#{project.id}/merge_requests/404/comments", user),
note: 'My comment'
expect(response.status).to eq(404)
end
end
- describe "GET :id/merge_request/:merge_request_id/comments" do
+ describe "GET :id/merge_requests/:merge_request_id/comments" do
it "should return merge_request comments ordered by created_at" do
- get api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
@@ -443,11 +453,33 @@ describe API::API, api: true do
end
it "should return a 404 error if merge_request_id not found" do
- get api("/projects/#{project.id}/merge_request/999/comments", user)
+ get api("/projects/#{project.id}/merge_requests/999/comments", user)
expect(response.status).to eq(404)
end
end
+ describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
+ it 'returns the issue that will be closed on merge' do
+ issue = create(:issue, project: project)
+ mr = merge_request.tap do |mr|
+ mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
+ end
+
+ get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns an empty array when there are no issues to be closed' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+ end
+
def mr_with_later_created_and_updated_at_time
merge_request
merge_request.created_at += 1.hour
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 8b177af4689..39f9a06fe1b 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -10,9 +10,32 @@ describe API::API, api: true do
let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) }
let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) }
+
+ # For testing the cross-reference of a private issue in a public issue
+ let(:private_user) { create(:user) }
+ let(:private_project) do
+ create(:project, namespace: private_user.namespace).
+ tap { |p| p.team << [private_user, :master] }
+ end
+ let(:private_issue) { create(:issue, project: private_project) }
+
+ let(:ext_proj) { create(:project, :public) }
+ let(:ext_issue) { create(:issue, project: ext_proj) }
+
+ let!(:cross_reference_note) do
+ create :note,
+ noteable: ext_issue, project: ext_proj,
+ note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ system: true
+ end
+
before { project.team << [user, :reporter] }
describe "GET /projects/:id/noteable/:noteable_id/notes" do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) }
+ end
+
context "when noteable is an Issue" do
it "should return an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
@@ -25,6 +48,24 @@ describe API::API, api: true do
get api("/projects/#{project.id}/issues/123/notes", user)
expect(response.status).to eq(404)
end
+
+ context "that references a private issue" do
+ it "should return an empty array" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to be_empty
+ end
+
+ context "and current user can view the note" do
+ it "should return an empty array" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(cross_reference_note.note)
+ end
+ end
+ end
end
context "when noteable is a Snippet" do
@@ -68,6 +109,21 @@ describe API::API, api: true do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user)
expect(response.status).to eq(404)
end
+
+ context "that references a private issue" do
+ it "should return a 404 error" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
+ expect(response.status).to eq(404)
+ end
+
+ context "and current user can view the note" do
+ it "should return an issue note by id" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
+ expect(response.status).to eq(200)
+ expect(json_response['body']).to eq(cross_reference_note.note)
+ end
+ end
+ end
end
context "when noteable is a Snippet" do
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
index 6358f6a2a4a..4301588b16a 100644
--- a/spec/requests/api/project_members_spec.rb
+++ b/spec/requests/api/project_members_spec.rb
@@ -6,8 +6,8 @@ describe API::API, api: true do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
- let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) }
+ let(:project_member) { create(:project_member, :master, user: user, project: project) }
+ let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
describe "GET /projects/:id/members" do
before { project_member }
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
new file mode 100644
index 00000000000..3722ddf5a33
--- /dev/null
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -0,0 +1,18 @@
+require 'rails_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ describe 'GET /projects/:project_id/snippets/:id' do
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ it 'always exposes expires_at as nil' do
+ admin = create(:admin)
+ snippet = create(:project_snippet, author: admin)
+
+ get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
+
+ expect(json_response).to have_key('expires_at')
+ expect(json_response['expires_at']).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 7f0f9454b10..a6699cdc81c 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -12,8 +12,8 @@ describe API::API, api: true do
let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
let(:project3) { create(:project, path: 'project3', creator_id: user.id, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') }
- let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
- let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) }
+ let(:project_member) { create(:project_member, :master, user: user, project: project) }
+ let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) }
let(:project3) do
create(:project,
@@ -90,6 +90,29 @@ describe API::API, api: true do
end
end
+ context 'and using the visibility filter' do
+ it 'should filter based on private visibility param' do
+ get api('/projects', user), { visibility: 'private' }
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
+ end
+
+ it 'should filter based on internal visibility param' do
+ get api('/projects', user), { visibility: 'internal' }
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
+ end
+
+ it 'should filter based on public visibility param' do
+ get api('/projects', user), { visibility: 'public' }
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count)
+ end
+ end
+
context 'and using sorting' do
before do
project2
@@ -353,6 +376,20 @@ describe API::API, api: true do
end
end
+ describe "POST /projects/:id/uploads" do
+ before { project }
+
+ it "uploads the file and returns its info" do
+ post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
+
+ expect(response.status).to be(201)
+ expect(json_response['alt']).to eq("dk")
+ expect(json_response['url']).to start_with("/uploads/")
+ expect(json_response['url']).to end_with("/dk.png")
+ expect(json_response['is_image']).to eq(true)
+ end
+ end
+
describe 'GET /projects/:id' do
before { project }
before { project_member }
@@ -382,6 +419,15 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
+ it 'should handle users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
describe 'permissions' do
context 'all projects' do
it 'Contains permission information' do
@@ -701,6 +747,42 @@ describe API::API, api: true do
end
end
+ describe "POST /projects/:id/share" do
+ let(:group) { create(:group) }
+
+ it "should share project with group" do
+ expect do
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ end.to change { ProjectGroupLink.count }.by(1)
+
+ expect(response.status).to eq 201
+ expect(json_response['group_id']).to eq group.id
+ expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
+ end
+
+ it "should return a 400 error when group id is not given" do
+ post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 400 error when access level is not given" do
+ post api("/projects/#{project.id}/share", user), group_id: group.id
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 400 error when sharing is disabled" do
+ project.namespace.update(share_with_group_lock: true)
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ expect(response.status).to eq 400
+ end
+
+ it "should return a 409 error when wrong params passed" do
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
+ expect(response.status).to eq 409
+ expect(json_response['message']).to eq 'Group access is not included in the list'
+ end
+ end
+
describe 'GET /projects/search/:query' do
let!(:query) { 'query'}
let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 4911cdd9da6..7cf4a01d76b 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -4,12 +4,13 @@ require 'mime/types'
describe API::API, api: true do
include ApiHelpers
include RepoHelpers
+ include WorkhorseHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
- let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
- let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
describe "GET /projects/:id/repository/tree" do
context "authorized user" do
@@ -91,21 +92,27 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/archive", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response.status).to eq(200)
- expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
end
it "should get the archive.zip" do
get api("/projects/#{project.id}/repository/archive.zip", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response.status).to eq(200)
- expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
end
it "should get the archive.tar.bz2" do
get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response.status).to eq(200)
- expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
end
it "should return 404 for invalid sha" do
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
new file mode 100644
index 00000000000..3af61d4b335
--- /dev/null
+++ b/spec/requests/api/runners_spec.rb
@@ -0,0 +1,464 @@
+require 'spec_helper'
+
+describe API::Runners, api: true do
+ include ApiHelpers
+
+ let(:admin) { create(:user, :admin) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+
+ let(:project) { create(:project, creator_id: user.id) }
+ let(:project2) { create(:project, creator_id: user.id) }
+
+ let!(:shared_runner) { create(:ci_runner, :shared) }
+ let!(:unused_specific_runner) { create(:ci_runner) }
+
+ let!(:specific_runner) do
+ create(:ci_runner).tap do |runner|
+ create(:ci_runner_project, runner: runner, project: project)
+ end
+ end
+
+ let!(:two_projects_runner) do
+ create(:ci_runner).tap do |runner|
+ create(:ci_runner_project, runner: runner, project: project)
+ create(:ci_runner_project, runner: runner, project: project2)
+ end
+ end
+
+ before do
+ # Set project access for users
+ create(:project_member, :master, user: user, project: project)
+ create(:project_member, :master, user: user, project: project2)
+ create(:project_member, :reporter, user: user2, project: project)
+ end
+
+ describe 'GET /runners' do
+ context 'authorized user' do
+ it 'should return user available runners' do
+ get api('/runners', user)
+ shared = json_response.any?{ |r| r['is_shared'] }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(shared).to be_falsey
+ end
+
+ it 'should filter runners by scope' do
+ get api('/runners?scope=active', user)
+ shared = json_response.any?{ |r| r['is_shared'] }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(shared).to be_falsey
+ end
+
+ it 'should avoid filtering if scope is invalid' do
+ get api('/runners?scope=unknown', user)
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return runners' do
+ get api('/runners')
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /runners/all' do
+ context 'authorized user' do
+ context 'with admin privileges' do
+ it 'should return all runners' do
+ get api('/runners/all', admin)
+ shared = json_response.any?{ |r| r['is_shared'] }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(shared).to be_truthy
+ end
+ end
+
+ context 'without admin privileges' do
+ it 'should not return runners list' do
+ get api('/runners/all', user)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ it 'should filter runners by scope' do
+ get api('/runners/all?scope=specific', admin)
+ shared = json_response.any?{ |r| r['is_shared'] }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(shared).to be_falsey
+ end
+
+ it 'should avoid filtering if scope is invalid' do
+ get api('/runners?scope=unknown', admin)
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return runners' do
+ get api('/runners')
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /runners/:id' do
+ context 'admin user' do
+ context 'when runner is shared' do
+ it "should return runner's details" do
+ get api("/runners/#{shared_runner.id}", admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response['description']).to eq(shared_runner.description)
+ end
+ end
+
+ context 'when runner is not shared' do
+ it "should return runner's details" do
+ get api("/runners/#{specific_runner.id}", admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response['description']).to eq(specific_runner.description)
+ end
+ end
+
+ it 'should return 404 if runner does not exists' do
+ get api('/runners/9999', admin)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context "runner project's administrative user" do
+ context 'when runner is not shared' do
+ it "should return runner's details" do
+ get api("/runners/#{specific_runner.id}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['description']).to eq(specific_runner.description)
+ end
+ end
+
+ context 'when runner is shared' do
+ it "should return runner's details" do
+ get api("/runners/#{shared_runner.id}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['description']).to eq(shared_runner.description)
+ end
+ end
+ end
+
+ context 'other authorized user' do
+ it "should not return runner's details" do
+ get api("/runners/#{specific_runner.id}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it "should not return runner's details" do
+ get api("/runners/#{specific_runner.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'PUT /runners/:id' do
+ context 'admin user' do
+ context 'when runner is shared' do
+ it 'should update runner' do
+ description = shared_runner.description
+ active = shared_runner.active
+
+ put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active,
+ tag_list: ['ruby2.1', 'pgsql', 'mysql']
+ shared_runner.reload
+
+ expect(response.status).to eq(200)
+ expect(shared_runner.description).to eq("#{description}_updated")
+ expect(shared_runner.active).to eq(!active)
+ expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
+ end
+ end
+
+ context 'when runner is not shared' do
+ it 'should update runner' do
+ description = specific_runner.description
+ put api("/runners/#{specific_runner.id}", admin), description: 'test'
+ specific_runner.reload
+
+ expect(response.status).to eq(200)
+ expect(specific_runner.description).to eq('test')
+ expect(specific_runner.description).not_to eq(description)
+ end
+ end
+
+ it 'should return 404 if runner does not exists' do
+ put api('/runners/9999', admin), description: 'test'
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user' do
+ context 'when runner is shared' do
+ it 'should not update runner' do
+ put api("/runners/#{shared_runner.id}", user)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when runner is not shared' do
+ it 'should not update runner without access to it' do
+ put api("/runners/#{specific_runner.id}", user2)
+
+ expect(response.status).to eq(403)
+ end
+
+ it 'should update runner with access to it' do
+ description = specific_runner.description
+ put api("/runners/#{specific_runner.id}", admin), description: 'test'
+ specific_runner.reload
+
+ expect(response.status).to eq(200)
+ expect(specific_runner.description).to eq('test')
+ expect(specific_runner.description).not_to eq(description)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not delete runner' do
+ put api("/runners/#{specific_runner.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'DELETE /runners/:id' do
+ context 'admin user' do
+ context 'when runner is shared' do
+ it 'should delete runner' do
+ expect do
+ delete api("/runners/#{shared_runner.id}", admin)
+ end.to change{ Ci::Runner.shared.count }.by(-1)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when runner is not shared' do
+ it 'should delete unused runner' do
+ expect do
+ delete api("/runners/#{unused_specific_runner.id}", admin)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ expect(response.status).to eq(200)
+ end
+
+ it 'should delete used runner' do
+ expect do
+ delete api("/runners/#{specific_runner.id}", admin)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ it 'should return 404 if runner does not exists' do
+ delete api('/runners/9999', admin)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user' do
+ context 'when runner is shared' do
+ it 'should not delete runner' do
+ delete api("/runners/#{shared_runner.id}", user)
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when runner is not shared' do
+ it 'should not delete runner without access to it' do
+ delete api("/runners/#{specific_runner.id}", user2)
+ expect(response.status).to eq(403)
+ end
+
+ it 'should not delete runner with more than one associated project' do
+ delete api("/runners/#{two_projects_runner.id}", user)
+ expect(response.status).to eq(403)
+ end
+
+ it 'should delete runner for one owned project' do
+ expect do
+ delete api("/runners/#{specific_runner.id}", user)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not delete runner' do
+ delete api("/runners/#{specific_runner.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/runners' do
+ context 'authorized user with master privileges' do
+ it "should return project's runners" do
+ get api("/projects/#{project.id}/runners", user)
+ shared = json_response.any?{ |r| r['is_shared'] }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(shared).to be_truthy
+ end
+ end
+
+ context 'authorized user without master privileges' do
+ it "should not return project's runners" do
+ get api("/projects/#{project.id}/runners", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it "should not return project's runners" do
+ get api("/projects/#{project.id}/runners")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/runners' do
+ context 'authorized user' do
+ it 'should enable specific runner' do
+ specific_runner2 = create(:ci_runner).tap do |runner|
+ create(:ci_runner_project, runner: runner, project: project2)
+ end
+
+ expect do
+ post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
+ end.to change{ project.runners.count }.by(+1)
+ expect(response.status).to eq(201)
+ end
+
+ it 'should avoid changes when enabling already enabled runner' do
+ expect do
+ post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
+ end.to change{ project.runners.count }.by(0)
+ expect(response.status).to eq(201)
+ end
+
+ it 'should not enable shared runner' do
+ post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id
+
+ expect(response.status).to eq(403)
+ end
+
+ context 'user is admin' do
+ it 'should enable any specific runner' do
+ expect do
+ post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
+ end.to change{ project.runners.count }.by(+1)
+ expect(response.status).to eq(201)
+ end
+ end
+
+ context 'user is not admin' do
+ it 'should not enable runner without access to' do
+ post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ it 'should raise an error when no runner_id param is provided' do
+ post api("/projects/#{project.id}/runners", admin)
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'authorized user without permissions' do
+ it 'should not enable runner' do
+ post api("/projects/#{project.id}/runners", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not enable runner' do
+ post api("/projects/#{project.id}/runners")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/runners/:runner_id' do
+ context 'authorized user' do
+ context 'when runner have more than one associated projects' do
+ it "should disable project's runner" do
+ expect do
+ delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
+ end.to change{ project.runners.count }.by(-1)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when runner have one associated projects' do
+ it "should not disable project's runner" do
+ expect do
+ delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
+ end.to change{ project.runners.count }.by(0)
+ expect(response.status).to eq(403)
+ end
+ end
+
+ it 'should return 404 is runner is not found' do
+ delete api("/projects/#{project.id}/runners/9999", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user without permissions' do
+ it "should not disable project's runner" do
+ delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it "should not disable project's runner" do
+ delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 17f2643fd45..a15be07ed57 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -8,8 +8,8 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
- let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
- let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
describe "GET /projects/:id/repository/tags" do
let(:tag_name) { project.repository.tag_names.sort.reverse.first }
@@ -65,6 +65,27 @@ describe API::API, api: true do
end
end
+ describe 'DELETE /projects/:id/repository/tags/:tag_name' do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true)
+ end
+
+ context 'delete tag' do
+ it 'should delete an existing tag' do
+ delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
+ expect(response.status).to eq(200)
+ expect(json_response['tag_name']).to eq(tag_name)
+ end
+
+ it 'should raise 404 if the tag does not exist' do
+ delete api("/projects/#{project.id}/repository/tags/foobar", user)
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
context 'annotated tag' do
it 'should create a new annotated tag' do
# Identity must be set in .gitconfig to create annotated tag.
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 314bd7ddc59..0510b77a39b 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -3,11 +3,19 @@ require 'spec_helper'
describe API::API do
include ApiHelpers
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:trigger_token) { 'secure_token' }
+ let!(:trigger_token_2) { 'secure_token_2' }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
+ let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
+ let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) }
+ let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
+
describe 'POST /projects/:project_id/trigger' do
- let!(:trigger_token) { 'secure token' }
- let!(:project) { FactoryGirl.create(:project) }
- let!(:project2) { FactoryGirl.create(:empty_project) }
- let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) }
+ let!(:project2) { create(:empty_project) }
let(:options) do
{
token: trigger_token
@@ -77,4 +85,127 @@ describe API::API do
end
end
end
+
+ describe 'GET /projects/:id/triggers' do
+ context 'authenticated user with valid permissions' do
+ it 'should return list of triggers' do
+ get api("/projects/#{project.id}/triggers", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a(Array)
+ expect(json_response[0]).to have_key('token')
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'should not return triggers list' do
+ get api("/projects/#{project.id}/triggers", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'should not return triggers list' do
+ get api("/projects/#{project.id}/triggers")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/triggers/:token' do
+ context 'authenticated user with valid permissions' do
+ it 'should return trigger details' do
+ get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a(Hash)
+ end
+
+ it 'should respond with 404 Not Found if requesting non-existing trigger' do
+ get api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'should not return triggers list' do
+ get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'should not return triggers list' do
+ get api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/triggers' do
+ context 'authenticated user with valid permissions' do
+ it 'should create trigger' do
+ expect do
+ post api("/projects/#{project.id}/triggers", user)
+ end.to change{project.triggers.count}.by(1)
+
+ expect(response.status).to eq(201)
+ expect(json_response).to be_a(Hash)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'should not create trigger' do
+ post api("/projects/#{project.id}/triggers", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'should not create trigger' do
+ post api("/projects/#{project.id}/triggers")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/triggers/:token' do
+ context 'authenticated user with valid permissions' do
+ it 'should delete trigger' do
+ expect do
+ delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+ end.to change{project.triggers.count}.by(-1)
+ expect(response.status).to eq(200)
+ end
+
+ it 'should respond with 404 Not Found if requesting non-existing trigger' do
+ delete api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'should not delete trigger' do
+ delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'should not delete trigger' do
+ delete api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 4f278551d07..679227bf881 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -8,6 +8,8 @@ describe API::API, api: true do
let(:key) { create(:key, user: user) }
let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_user) }
+ let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
+ let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
describe "GET /users" do
context "when unauthenticated" do
@@ -45,6 +47,8 @@ describe API::API, api: true do
expect(json_response.first.keys).to include 'identities'
expect(json_response.first.keys).to include 'can_create_project'
expect(json_response.first.keys).to include 'two_factor_enabled'
+ expect(json_response.first.keys).to include 'last_sign_in_at'
+ expect(json_response.first.keys).to include 'confirmed_at'
end
end
end
@@ -116,6 +120,26 @@ describe API::API, api: true do
expect(response.status).to eq(201)
end
+ it 'creates non-external users by default' do
+ post api("/users", admin), attributes_for(:user)
+ expect(response.status).to eq(201)
+
+ user_id = json_response['id']
+ new_user = User.find(user_id)
+ expect(new_user).not_to eq nil
+ expect(new_user.external).to be_falsy
+ end
+
+ it 'should allow an external user to be created' do
+ post api("/users", admin), attributes_for(:user, external: true)
+ expect(response.status).to eq(201)
+
+ user_id = json_response['id']
+ new_user = User.find(user_id)
+ expect(new_user).not_to eq nil
+ expect(new_user.external).to be_truthy
+ end
+
it "should not create user with invalid email" do
post api('/users', admin),
email: 'invalid email',
@@ -258,6 +282,13 @@ describe API::API, api: true do
expect(user.reload.admin).to eq(true)
end
+ it "should update external status" do
+ put api("/users/#{user.id}", admin), { external: true }
+ expect(response.status).to eq 200
+ expect(json_response['external']).to eq(true)
+ expect(user.reload.external?).to be_truthy
+ end
+
it "should not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
expect(response.status).to eq(200)
@@ -783,6 +814,12 @@ describe API::API, api: true do
expect(user.reload.state).to eq('blocked')
end
+ it 'should not re-block ldap blocked users' do
+ put api("/users/#{ldap_blocked_user.id}/block", admin)
+ expect(response.status).to eq(403)
+ expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
+ end
+
it 'should not be available for non admin users' do
put api("/users/#{user.id}/block", user)
expect(response.status).to eq(403)
@@ -797,7 +834,9 @@ describe API::API, api: true do
end
describe 'PUT /user/:id/unblock' do
+ let(:blocked_user) { create(:user, state: 'blocked') }
before { admin }
+
it 'should unblock existing user' do
put api("/users/#{user.id}/unblock", admin)
expect(response.status).to eq(200)
@@ -805,12 +844,15 @@ describe API::API, api: true do
end
it 'should unblock a blocked user' do
- put api("/users/#{user.id}/block", admin)
+ put api("/users/#{blocked_user.id}/unblock", admin)
expect(response.status).to eq(200)
- expect(user.reload.state).to eq('blocked')
- put api("/users/#{user.id}/unblock", admin)
- expect(response.status).to eq(200)
- expect(user.reload.state).to eq('active')
+ expect(blocked_user.reload.state).to eq('active')
+ end
+
+ it 'should not unblock ldap blocked users' do
+ put api("/users/#{ldap_blocked_user.id}/unblock", admin)
+ expect(response.status).to eq(403)
+ expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'should not be available for non admin users' do
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
new file mode 100644
index 00000000000..b1e1053d037
--- /dev/null
+++ b/spec/requests/api/variables_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
+ let!(:variable) { create(:ci_variable, project: project) }
+
+ describe 'GET /projects/:id/variables' do
+ context 'authorized user with proper permissions' do
+ it 'should return project variables' do
+ get api("/projects/#{project.id}/variables", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a(Array)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not return project variables' do
+ get api("/projects/#{project.id}/variables", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project variables' do
+ get api("/projects/#{project.id}/variables")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['value']).to eq(variable.value)
+ end
+
+ it 'should respond with 404 Not Found if requesting non-existing variable' do
+ get api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/variables' do
+ context 'authorized user with proper permissions' do
+ it 'should create variable' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
+ end.to change{project.variables.count}.by(1)
+
+ expect(response.status).to eq(201)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('VALUE_2')
+ end
+
+ it 'should not allow to duplicate variable key' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
+ end.to change{project.variables.count}.by(0)
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not create variable' do
+ post api("/projects/#{project.id}/variables", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not create variable' do
+ post api("/projects/#{project.id}/variables")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should update variable data' do
+ initial_variable = project.variables.first
+ value_before = initial_variable.value
+
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP'
+
+ updated_variable = project.variables.first
+
+ expect(response.status).to eq(200)
+ expect(value_before).to eq(variable.value)
+ expect(updated_variable.value).to eq('VALUE_1_UP')
+ end
+
+ it 'should responde with 404 Not Found if requesting non-existing variable' do
+ put api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not update variable' do
+ put api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not update variable' do
+ put api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should delete variable' do
+ expect do
+ delete api("/projects/#{project.id}/variables/#{variable.key}", user)
+ end.to change{project.variables.count}.by(-1)
+ expect(response.status).to eq(200)
+ end
+
+ it 'should responde with 404 Not Found if requesting non-existing variable' do
+ delete api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not delete variable' do
+ delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not delete variable' do
+ delete api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index c27e87c4acc..57d7eb927fd 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -101,31 +101,66 @@ describe Ci::API::API do
{ "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false },
])
end
+
+ it "returns dependent builds" do
+ commit = FactoryGirl.create(:ci_commit, project: project)
+ commit.create_builds('master', false, nil, nil)
+ commit.builds.where(stage: 'test').each(&:success)
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response["depends_on_builds"].count).to eq(2)
+ expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec")
+ end
+
+ %w(name version revision platform architecture).each do |param|
+ context "updates runner #{param}" do
+ let(:value) { "#{param}_value" }
+
+ subject { runner.read_attribute(param.to_sym) }
+
+ it do
+ post ci_api("/builds/register"), token: runner.token, info: { param => value }
+ expect(response.status).to eq(404)
+ runner.reload
+ is_expected.to eq(value)
+ end
+ end
+ end
end
describe "PUT /builds/:id" do
- let(:commit) { FactoryGirl.create(:ci_commit, project: project)}
- let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) }
+ let(:commit) {create(:ci_commit, project: project)}
+ let(:build) { create(:ci_build, :trace, commit: commit, runner_id: runner.id) }
- it "should update a running build" do
+ before do
build.run!
put ci_api("/builds/#{build.id}"), token: runner.token
+ end
+
+ it "should update a running build" do
expect(response.status).to eq(200)
end
- it 'Should not override trace information when no trace is given' do
- build.run!
- build.update!(trace: 'hello_world')
- put ci_api("/builds/#{build.id}"), token: runner.token
- expect(build.reload.trace).to eq 'hello_world'
+ it 'should not override trace information when no trace is given' do
+ expect(build.reload.trace).to eq 'BUILD TRACE'
+ end
+
+ context 'build has been erased' do
+ let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
+
+ it 'should respond with forbidden' do
+ expect(response.status).to eq 403
+ end
end
end
context "Artifacts" do
let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
- let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
- let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) }
+ let(:commit) { create(:ci_commit, project: project) }
+ let(:build) { create(:ci_build, commit: commit, runner_id: runner.id) }
let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
@@ -133,12 +168,10 @@ describe Ci::API::API do
let(:headers) { { "GitLab-Workhorse" => "1.0" } }
let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) }
+ before { build.run! }
+
describe "POST /builds/:id/artifacts/authorize" do
context "should authorize posting artifact to running build" do
- before do
- build.run!
- end
-
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
expect(response.status).to eq(200)
@@ -153,10 +186,6 @@ describe Ci::API::API do
end
context "should fail to post too large artifact" do
- before do
- build.run!
- end
-
it "using token as parameter" do
stub_application_setting(max_artifacts_size: 0)
post authorize_url, { token: build.token, filesize: 100 }, headers
@@ -170,26 +199,32 @@ describe Ci::API::API do
end
end
- context "should get denied" do
- it do
- post authorize_url, { token: 'invalid', filesize: 100 }
+ context 'authorization token is invalid' do
+ before { post authorize_url, { token: 'invalid', filesize: 100 } }
+
+ it 'should respond with forbidden' do
expect(response.status).to eq(403)
end
end
end
describe "POST /builds/:id/artifacts" do
- context "Disable sanitizer" do
+ context "disable sanitizer" do
before do
# by configuring this path we allow to pass temp file from any path
allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
end
- context "should post artifact to running build" do
- before do
- build.run!
+ context 'build has been erased' do
+ let(:build) { create(:ci_build, erased_at: Time.now) }
+ before { upload_artifacts(file_upload, headers_with_token) }
+
+ it 'should respond with forbidden' do
+ expect(response.status).to eq 403
end
+ end
+ context "should post artifact to running build" do
it "uses regual file post" do
upload_artifacts(file_upload, headers_with_token, false)
expect(response.status).to eq(201)
@@ -210,55 +245,83 @@ describe Ci::API::API do
end
end
- context "should fail to post too large artifact" do
+ context 'should post artifacts file and metadata file' do
+ let!(:artifacts) { file_upload }
+ let!(:metadata) { file_upload2 }
+
+ let(:stored_artifacts_file) { build.reload.artifacts_file.file }
+ let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
+
before do
- build.run!
+ post(post_url, post_data, headers_with_token)
+ end
+
+ context 'post data accelerated by workhorse is correct' do
+ let(:post_data) do
+ { 'file.path' => artifacts.path,
+ 'file.name' => artifacts.original_filename,
+ 'metadata.path' => metadata.path,
+ 'metadata.name' => metadata.original_filename }
+ end
+
+ it 'stores artifacts and artifacts metadata' do
+ expect(response.status).to eq(201)
+ expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
+ expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
+ end
+ end
+
+ context 'no artifacts file in post data' do
+ let(:post_data) do
+ { 'metadata' => metadata }
+ end
+
+ it 'is expected to respond with bad request' do
+ expect(response.status).to eq(400)
+ end
+
+ it 'does not store metadata' do
+ expect(stored_metadata_file).to be_nil
+ end
end
+ end
- it do
+ context "artifacts file is too large" do
+ it "should fail to post too large artifact" do
stub_application_setting(max_artifacts_size: 0)
upload_artifacts(file_upload, headers_with_token)
expect(response.status).to eq(413)
end
end
- context "should fail to post artifacts without file" do
- before do
- build.run!
- end
-
- it do
+ context "artifacts post request does not contain file" do
+ it "should fail to post artifacts without file" do
post post_url, {}, headers_with_token
expect(response.status).to eq(400)
end
end
- context "should fail to post artifacts without GitLab-Workhorse" do
- before do
- build.run!
- end
-
- it do
+ context 'GitLab Workhorse is not configured' do
+ it "should fail to post artifacts without GitLab-Workhorse" do
post post_url, { token: build.token }, {}
expect(response.status).to eq(403)
end
end
end
- context "should fail to post artifacts for outside of tmp path" do
+ context "artifacts are being stored outside of tmp path" do
before do
# by configuring this path we allow to pass file from @tmpdir only
# but all temporary files are stored in system tmp directory
@tmpdir = Dir.mktmpdir
allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
- build.run!
end
after do
FileUtils.remove_entry @tmpdir
end
- it do
+ it "should fail to post artifacts for outside of tmp path" do
upload_artifacts(file_upload, headers_with_token)
expect(response.status).to eq(400)
end
@@ -276,33 +339,37 @@ describe Ci::API::API do
end
end
- describe "DELETE /builds/:id/artifacts" do
- before do
- build.run!
- post delete_url, token: build.token, file: file_upload
- end
+ describe 'DELETE /builds/:id/artifacts' do
+ let(:build) { create(:ci_build, :artifacts) }
+ before { delete delete_url, token: build.token }
- it "should delete artifact build" do
- build.success
- delete delete_url, token: build.token
+ it 'should remove build artifacts' do
expect(response.status).to eq(200)
+ expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_metadata.exists?).to be_falsy
end
end
- describe "GET /builds/:id/artifacts" do
- before do
- build.run!
- end
+ describe 'GET /builds/:id/artifacts' do
+ before { get get_url, token: build.token }
- it "should download artifact" do
- build.update_attributes(artifacts_file: file_upload)
- get get_url, token: build.token
- expect(response.status).to eq(200)
+ context 'build has artifacts' do
+ let(:build) { create(:ci_build, :artifacts) }
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding'=>'binary',
+ 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ it 'should download artifact' do
+ expect(response.status).to eq(200)
+ expect(response.headers).to include download_headers
+ end
end
- it "should fail to download if no artifact uploaded" do
- get get_url, token: build.token
- expect(response.status).to eq(404)
+ context 'build does not has artifacts' do
+ it 'should respond with not found' do
+ expect(response.status).to eq(404)
+ end
end
end
end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index 5942aa7a1b5..db8189ffb79 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -51,6 +51,20 @@ describe Ci::API::API do
expect(response.status).to eq(400)
end
+
+ %w(name version revision platform architecture).each do |param|
+ context "creates runner with #{param} saved" do
+ let(:value) { "#{param}_value" }
+
+ subject { Ci::Runner.first.read_attribute(param.to_sym) }
+
+ it do
+ post ci_api("/runners/register"), token: registration_token, info: { param => value }
+ expect(response.status).to eq(201)
+ is_expected.to eq(value)
+ end
+ end
+ end
end
describe "DELETE /runners/delete" do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 82f62a8709c..538f44e4f3f 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -80,6 +80,7 @@ describe ProjectsController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys')
end
it 'to #update' do
@@ -320,12 +321,12 @@ describe Projects::HooksController, 'routing' do
end
end
-# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/}
+# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/}
describe Projects::CommitController, 'routing' do
it 'to #show' do
- expect(get('/gitlab/gitlabhq/commit/4246fb')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb')
- expect(get('/gitlab/gitlabhq/commit/4246fb.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'diff')
- expect(get('/gitlab/gitlabhq/commit/4246fb.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'patch')
+ expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd')
+ expect(get('/gitlab/gitlabhq/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff')
+ expect(get('/gitlab/gitlabhq/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch')
expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
end
end
@@ -434,6 +435,18 @@ describe Projects::TreeController, 'routing' do
end
end
+# project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/}
+# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/}
+describe Projects::FindFileController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ end
+
+ it 'to #list' do
+ expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ end
+end
+
describe Projects::BlobController, 'routing' do
it 'to #edit' do
expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to(
@@ -483,11 +496,11 @@ end
describe Projects::ForksController, 'routing' do
it 'to #new' do
- expect(get('/gitlab/gitlabhq/fork/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
it 'to #create' do
- expect(post('/gitlab/gitlabhq/fork')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index dfa18f69e05..1527eddfa48 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -137,7 +137,6 @@ end
# keys GET /keys(.:format) keys#index
# POST /keys(.:format) keys#create
-# new_key GET /keys/new(.:format) keys#new
# edit_key GET /keys/:id/edit(.:format) keys#edit
# key GET /keys/:id(.:format) keys#show
# PUT /keys/:id(.:format) keys#update
@@ -151,10 +150,6 @@ describe Profiles::KeysController, "routing" do
expect(post("/profile/keys")).to route_to('profiles/keys#create')
end
- it "to #new" do
- expect(get("/profile/keys/new")).to route_to('profiles/keys#new')
- end
-
it "to #edit" do
expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1')
end
diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb
deleted file mode 100644
index bd871605c66..00000000000
--- a/spec/services/archive_repository_service_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require 'spec_helper'
-
-describe ArchiveRepositoryService, services: true do
- let(:project) { create(:project) }
- subject { ArchiveRepositoryService.new(project, "master", "zip") }
-
- describe "#execute" do
- it "cleans old archives" do
- expect(RepositoryArchiveCacheWorker).to receive(:perform_async)
-
- subject.execute(timeout: 0.0)
- end
-
- context "when the repository doesn't have an archive file path" do
- before do
- allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
- end
-
- it "raises an error" do
- expect { subject.execute(timeout: 0.0) }.to raise_error(RuntimeError)
- end
- end
-
- end
-end
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
new file mode 100644
index 00000000000..1fca3628686
--- /dev/null
+++ b/spec/services/ci/create_builds_service_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Ci::CreateBuildsService, services: true do
+ let(:commit) { create(:ci_commit) }
+ let(:user) { create(:user) }
+
+ describe '#execute' do
+ # Using stubbed .gitlab-ci.yml created in commit factory
+ #
+
+ subject do
+ described_class.new.execute(commit, 'test', 'master', nil, user, nil, status)
+ end
+
+ context 'next builds available' do
+ let(:status) { 'success' }
+
+ it { is_expected.to be_an_instance_of Array }
+ it { is_expected.to all(be_an_instance_of Ci::Build) }
+ end
+
+ context 'builds skipped' do
+ let(:status) { 'skipped' }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/delete_tag_service_spec.rb
new file mode 100644
index 00000000000..5b7ba521812
--- /dev/null
+++ b/spec/services/delete_tag_service_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe DeleteTagService, services: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ let(:tag) { double(:tag, name: '8.5', target: 'abc123') }
+
+ describe '#execute' do
+ before do
+ allow(repository).to receive(:find_tag).and_return(tag)
+ end
+
+ it 'removes the tag' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
+ and_return(true)
+
+ expect(repository).to receive(:before_remove_tag)
+ expect(service).to receive(:success)
+
+ service.execute('8.5')
+ end
+ end
+end
diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb
new file mode 100644
index 00000000000..a65938fa03b
--- /dev/null
+++ b/spec/services/delete_user_service_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe DeleteUserService, services: true do
+ describe "Deletes a user and all their personal projects" do
+ let!(:user) { create(:user) }
+ let!(:current_user) { create(:user) }
+ let!(:namespace) { create(:namespace, owner: user) }
+ let!(:project) { create(:project, namespace: namespace) }
+
+ context 'no options are given' do
+ it 'deletes the user' do
+ DeleteUserService.new(current_user).execute(user)
+
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'will delete the project in the near future' do
+ expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once
+
+ DeleteUserService.new(current_user).execute(user)
+ end
+ end
+
+ context "solo owned groups present" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
+
+ before do
+ solo_owned.group_members = [member]
+ DeleteUserService.new(current_user).execute(user)
+ end
+
+ it 'does not delete the user' do
+ expect(User.find(user.id)).to eq user
+ end
+ end
+
+ context "deletions with solo owned groups" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
+
+ before do
+ solo_owned.group_members = [member]
+ DeleteUserService.new(current_user).execute(user, delete_solo_owned_groups: true)
+ end
+
+ it 'deletes solo owned groups' do
+ expect { Project.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'deletes the user' do
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index c1080ef190a..8490a729e51 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -5,7 +5,6 @@ describe GitPushService, services: true do
let(:user) { create :user }
let(:project) { create :project }
- let(:service) { GitPushService.new }
before do
@blankrev = Gitlab::Git::BLANK_SHA
@@ -15,34 +14,70 @@ describe GitPushService, services: true do
end
describe 'Push branches' do
+
+ let(:oldrev) { @oldrev }
+ let(:newrev) { @newrev }
+
+ subject do
+ execute_service(project, user, oldrev, newrev, @ref )
+ end
+
context 'new branch' do
- subject do
- service.execute(project, user, @blankrev, @newrev, @ref)
- end
+
+ let(:oldrev) { @blankrev }
it { is_expected.to be_truthy }
+
+ it 'flushes general cached data' do
+ expect(project.repository).to receive(:expire_cache).
+ with('master', newrev)
+
+ subject
+ end
+
+ it 'flushes the visible content cache' do
+ expect(project.repository).to receive(:expire_has_visible_content_cache)
+
+ subject
+ end
end
context 'existing branch' do
- subject do
- service.execute(project, user, @oldrev, @newrev, @ref)
- end
it { is_expected.to be_truthy }
+
+ it 'flushes general cached data' do
+ expect(project.repository).to receive(:expire_cache).
+ with('master', newrev)
+
+ subject
+ end
end
context 'rm branch' do
- subject do
- service.execute(project, user, @oldrev, @blankrev, @ref)
- end
+
+ let(:newrev) { @blankrev }
it { is_expected.to be_truthy }
+
+ it 'flushes the visible content cache' do
+ expect(project.repository).to receive(:expire_has_visible_content_cache)
+
+ subject
+ end
+
+ it 'flushes general cached data' do
+ expect(project.repository).to receive(:expire_cache).
+ with('master', newrev)
+
+ subject
+ end
end
end
describe "Git Push Data" do
before do
- service.execute(project, user, @oldrev, @newrev, @ref)
+ service = execute_service(project, user, @oldrev, @newrev, @ref )
@push_data = service.push_data
@commit = project.commit(@newrev)
end
@@ -104,31 +139,49 @@ describe GitPushService, services: true do
describe "Push Event" do
before do
- service.execute(project, user, @oldrev, @newrev, @ref)
+ service = execute_service(project, user, @oldrev, @newrev, @ref )
@event = Event.last
+ @push_data = service.push_data
end
it { expect(@event).not_to be_nil }
it { expect(@event.project).to eq(project) }
it { expect(@event.action).to eq(Event::PUSHED) }
- it { expect(@event.data).to eq(service.push_data) }
+ it { expect(@event.data).to eq(@push_data) }
context "Updates merge requests" do
it "when pushing a new branch for the first time" do
expect(project).to receive(:update_merge_requests).
with(@blankrev, 'newrev', 'refs/heads/master', user)
- service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master')
+ execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+ end
+ end
+ end
+
+ describe "Updates main language" do
+
+ context "before push" do
+ it { expect(project.main_language).to eq(nil) }
+ end
+
+ context "after push" do
+ before do
+ @service = execute_service(project, user, @oldrev, @newrev, @ref)
end
+
+ it { expect(@service.update_main_language).to eq(true) }
+ it { expect(project.main_language).to eq("Ruby") }
end
end
- describe "Web Hooks" do
- context "execute web hooks" do
+
+ describe "Webhooks" do
+ context "execute webhooks" do
it "when pushing a branch for the first time" do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false })
- service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master')
+ execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
end
it "when pushing a branch for the first time with default branch protection disabled" do
@@ -137,7 +190,7 @@ describe GitPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
expect(project.protected_branches).not_to receive(:create)
- service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master')
+ execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
end
it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do
@@ -146,12 +199,12 @@ describe GitPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true })
- service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master')
+ execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
end
it "when pushing new commits to existing branch" do
expect(project).to receive(:execute_hooks)
- service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master')
+ execute_service(project, user, 'oldrev', 'newrev', 'refs/heads/master' )
end
end
end
@@ -162,19 +215,23 @@ 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
it "creates a note if a pushed commit mentions an issue" do
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author)
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, user, @oldrev, @newrev, @ref )
end
it "only creates a cross-reference note if one doesn't already exist" do
@@ -182,7 +239,7 @@ describe GitPushService, services: true do
expect(SystemNoteService).not_to receive(:cross_reference).with(issue, commit, commit_author)
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, user, @oldrev, @newrev, @ref )
end
it "defaults to the pushing user if the commit's author is not known" do
@@ -192,7 +249,7 @@ describe GitPushService, services: true do
)
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, user)
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, user, @oldrev, @newrev, @ref )
end
it "finds references in the first push to a non-default branch" do
@@ -201,7 +258,7 @@ describe GitPushService, services: true do
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author)
- service.execute(project, user, @blankrev, @newrev, 'refs/heads/other')
+ execute_service(project, user, @blankrev, @newrev, 'refs/heads/other' )
end
end
@@ -221,22 +278,24 @@ describe GitPushService, services: true do
allow(project.repository).to receive(:commits_between).
and_return([closing_commit])
+
+ project.team << [commit_author, :master]
end
context "to default branches" do
it "closes issues" do
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(Issue.find(issue.id)).to be_closed
end
it "adds a note indicating that the issue is now closed" do
expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit)
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
end
it "doesn't create additional cross-reference notes" do
expect(SystemNoteService).not_to receive(:cross_reference)
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
end
it "doesn't close issues when external issue tracker is in use" do
@@ -244,7 +303,7 @@ describe GitPushService, services: true do
# The push still shouldn't create cross-reference notes.
expect do
- service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf')
+ execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' )
end.not_to change { Note.where(project_id: project.id, system: true).count }
end
end
@@ -257,16 +316,15 @@ describe GitPushService, services: true do
it "creates cross-reference notes" do
expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author)
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, user, @oldrev, @newrev, @ref )
end
it "doesn't close issues" do
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, user, @oldrev, @newrev, @ref )
expect(Issue.find(issue.id)).to be_opened
end
end
- # EE-only tests
context "for jira issue tracker" do
include JiraServiceHelper
@@ -298,7 +356,7 @@ describe GitPushService, services: true do
let(:message) { "this is some work.\n\nrelated to JIRA-1" }
it "should initiate one api call to jira server to mention the issue" do
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, user, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
body: /mentioned this issue in/
@@ -316,7 +374,7 @@ describe GitPushService, services: true do
}
}.to_json
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
body: transition_body
).once
@@ -327,7 +385,7 @@ describe GitPushService, services: true do
body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
}.to_json
- service.execute(project, user, @oldrev, @newrev, @ref)
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
body: comment_body
).once
@@ -346,7 +404,52 @@ describe GitPushService, services: true do
end
it 'push to first branch updates HEAD' do
- service.execute(project, user, @blankrev, @newrev, new_ref)
+ execute_service(project, user, @blankrev, @newrev, new_ref )
+ end
+ end
+
+ describe "housekeeping" do
+ let(:housekeeping) { Projects::HousekeepingService.new(project) }
+
+ before do
+ allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping)
end
+
+ it 'does not perform housekeeping when not needed' do
+ expect(housekeeping).not_to receive(:execute)
+
+ execute_service(project, user, @oldrev, @newrev, @ref)
+ end
+
+ context 'when housekeeping is needed' do
+ before do
+ allow(housekeeping).to receive(:needed?).and_return(true)
+ end
+
+ it 'performs housekeeping' do
+ expect(housekeeping).to receive(:execute)
+
+ execute_service(project, user, @oldrev, @newrev, @ref)
+ end
+
+ it 'does not raise an exception' do
+ allow(housekeeping).to receive(:try_obtain_lease).and_return(false)
+
+ execute_service(project, user, @oldrev, @newrev, @ref)
+ end
+ end
+
+
+ it 'increments the push counter' do
+ expect(housekeeping).to receive(:increment!)
+
+ execute_service(project, user, @oldrev, @newrev, @ref)
+ end
+ end
+
+ def execute_service(project, user, oldrev, newrev, ref)
+ service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
+ service.execute
+ service
end
end
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index b982274c529..cc780587e74 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -78,8 +78,8 @@ describe GitTagPushService, services: true do
end
end
- describe "Web Hooks" do
- context "execute web hooks" do
+ describe "Webhooks" do
+ context "execute webhooks" do
it "when pushing tags" do
expect(project).to receive(:execute_hooks)
service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0')
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 3a8daf28f5e..62b25709a5d 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -5,6 +5,7 @@ describe Issues::CloseService, services: true do
let(:user2) { create(:user) }
let(:issue) { create(:issue, assignee: user2) }
let(:project) { issue.project }
+ let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
before do
project.team << [user, :master]
@@ -32,6 +33,10 @@ describe Issues::CloseService, services: true do
note = @issue.notes.last
expect(note.note).to include "Status changed to closed"
end
+
+ it 'marks todos as done' do
+ expect(todo.reload).to be_done
+ end
end
context "external issue tracker" do
@@ -42,6 +47,7 @@ describe Issues::CloseService, services: true do
it { expect(@issue).to be_valid }
it { expect(@issue).to be_opened }
+ it { expect(todo.reload).to be_pending }
end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 2148d091a57..5e7915db7e1 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -3,14 +3,18 @@ require 'spec_helper'
describe Issues::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
+ let(:assignee) { create(:user) }
describe :execute do
- context "valid params" do
+ context 'valid params' do
before do
project.team << [user, :master]
+ project.team << [assignee, :master]
+
opts = {
title: 'Awesome issue',
- description: 'please fix'
+ description: 'please fix',
+ assignee: assignee
}
@issue = Issues::CreateService.new(project, user, opts).execute
@@ -18,6 +22,21 @@ describe Issues::CreateService, services: true do
it { expect(@issue).to be_valid }
it { expect(@issue.title).to eq('Awesome issue') }
+ it { expect(@issue.assignee).to eq assignee }
+
+ it 'creates a pending todo for new assignee' do
+ attributes = {
+ project: project,
+ author: user,
+ user: assignee,
+ target_id: @issue.id,
+ target_type: @issue.class.name,
+ action: Todo::ASSIGNED,
+ state: :pending
+ }
+
+ expect(Todo.where(attributes).count).to eq 1
+ end
end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 87da0e9618b..4ffe753fef5 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -6,6 +6,7 @@ describe Issues::UpdateService, services: true do
let(:user3) { create(:user) }
let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) }
let(:label) { create(:label) }
+ let(:label2) { create(:label) }
let(:project) { issue.project }
before do
@@ -48,7 +49,7 @@ describe Issues::UpdateService, services: true do
it { expect(@issue.assignee).to eq(user2) }
it { expect(@issue).to be_closed }
it { expect(@issue.labels.count).to eq(1) }
- it { expect(@issue.labels.first.title).to eq('Bug') }
+ it { expect(@issue.labels.first.title).to eq(label.name) }
it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
deliveries = ActionMailer::Base.deliveries
@@ -80,6 +81,116 @@ describe Issues::UpdateService, services: true do
end
end
+ context 'todos' do
+ let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
+
+ context 'when the title change' do
+ before do
+ update_issue({ title: 'New title' })
+ end
+
+ it 'marks pending todos as done' do
+ expect(todo.reload.done?).to eq true
+ end
+ end
+
+ context 'when the description change' do
+ before do
+ update_issue({ description: 'Also please fix' })
+ end
+
+ it 'marks todos as done' do
+ expect(todo.reload.done?).to eq true
+ end
+ end
+
+ context 'when is reassigned' do
+ before do
+ update_issue({ assignee: user2 })
+ end
+
+ it 'marks previous assignee todos as done' do
+ expect(todo.reload.done?).to eq true
+ end
+
+ it 'creates a todo for new assignee' do
+ attributes = {
+ project: project,
+ author: user,
+ user: user2,
+ target_id: issue.id,
+ target_type: issue.class.name,
+ action: Todo::ASSIGNED,
+ state: :pending
+ }
+
+ expect(Todo.where(attributes).count).to eq 1
+ end
+ end
+
+ context 'when the milestone change' do
+ before do
+ update_issue({ milestone: create(:milestone) })
+ end
+
+ it 'marks todos as done' do
+ expect(todo.reload.done?).to eq true
+ end
+ end
+
+ context 'when the labels change' do
+ before do
+ update_issue({ label_ids: [label.id] })
+ end
+
+ it 'marks todos as done' do
+ expect(todo.reload.done?).to eq true
+ end
+ end
+ end
+
+ context 'when the issue is relabeled' do
+ let!(:non_subscriber) { create(:user) }
+ let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+
+ it 'sends notifications for subscribers of newly added labels' do
+ opts = { label_ids: [label.id] }
+
+ perform_enqueued_jobs do
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+
+ context 'when issue has the `label` label' do
+ before { issue.labels << label }
+
+ it 'does not send notifications for existing labels' do
+ opts = { label_ids: [label.id, label2.id] }
+
+ perform_enqueued_jobs do
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ end
+
+ should_not_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+
+ it 'does not send notifications for removed labels' do
+ opts = { label_ids: [label2.id] }
+
+ perform_enqueued_jobs do
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ end
+
+ should_not_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+ end
+ end
+
context 'when Issue has tasks' do
before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
@@ -144,6 +255,5 @@ describe Issues::UpdateService, services: true do
end
end
end
-
end
end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 50d0c288790..8443a00e70c 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -5,6 +5,7 @@ describe MergeRequests::CloseService, services: true do
let(:user2) { create(:user) }
let(:merge_request) { create(:merge_request, assignee: user2) }
let(:project) { merge_request.project }
+ let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
before do
project.team << [user, :master]
@@ -41,6 +42,10 @@ describe MergeRequests::CloseService, services: true do
note = @merge_request.notes.last
expect(note.note).to include 'Status changed to closed'
end
+
+ it 'marks todos as done' do
+ expect(todo.reload).to be_done
+ end
end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index be8f1676eeb..120f4d6a669 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe MergeRequests::CreateService, services: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:assignee) { create(:user) }
describe :execute do
context 'valid params' do
@@ -14,10 +15,12 @@ describe MergeRequests::CreateService, services: true do
target_branch: 'master'
}
end
+
let(:service) { MergeRequests::CreateService.new(project, user, opts) }
before do
project.team << [user, :master]
+ project.team << [assignee, :developer]
allow(service).to receive(:execute_hooks)
@merge_request = service.execute
@@ -25,10 +28,49 @@ describe MergeRequests::CreateService, services: true do
it { expect(@merge_request).to be_valid }
it { expect(@merge_request.title).to eq('Awesome merge_request') }
+ it { expect(@merge_request.assignee).to be_nil }
it 'should execute hooks with default action' do
expect(service).to have_received(:execute_hooks).with(@merge_request)
end
+
+ it 'does not creates todos' do
+ attributes = {
+ project: project,
+ target_id: @merge_request.id,
+ target_type: @merge_request.class.name
+ }
+
+ expect(Todo.where(attributes).count).to be_zero
+ end
+
+ context 'when merge request is assigned to someone' do
+ let(:opts) do
+ {
+ title: 'Awesome merge_request',
+ description: 'please fix',
+ source_branch: 'feature',
+ target_branch: 'master',
+ assignee: assignee
+ }
+ end
+
+ it { expect(@merge_request.assignee).to eq assignee }
+
+ it 'creates a todo for new assignee' do
+ attributes = {
+ project: project,
+ author: user,
+ user: assignee,
+ target_id: @merge_request.id,
+ target_type: @merge_request.class.name,
+ action: Todo::ASSIGNED,
+ state: :pending
+ }
+
+ expect(Todo.where(attributes).count).to eq 1
+ end
+ end
end
end
end
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index 449cecaa789..52a302e0e1a 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
let(:mr_merge_if_green_enabled) do
create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
- source_branch: "source_branch", target_branch: project.default_branch,
+ source_branch: "master", target_branch: 'feature',
source_project: project, target_project: project, state: "opened")
end
@@ -54,14 +54,84 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end
describe "#trigger" do
- let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
+ context 'build with ref' do
+ let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
- it "merges all merge requests with merge when build succeeds enabled" do
- allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
- allow(ci_commit).to receive(:success?).and_return(true)
+ it "merges all merge requests with merge when build succeeds enabled" do
+ allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
+ allow(ci_commit).to receive(:success?).and_return(true)
+
+ expect(MergeWorker).to receive(:perform_async)
+ service.trigger(build)
+ end
+ end
+
+ context 'triggered by an old build' do
+ let(:old_build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
+ let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
+
+ it "merges all merge requests with merge when build succeeds enabled" do
+ allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
+ allow(ci_commit).to receive(:success?).and_return(true)
+ allow(old_build).to receive(:sha).and_return('1234abcdef')
+
+ expect(MergeWorker).to_not receive(:perform_async)
+ service.trigger(old_build)
+ end
+ end
+
+ context 'commit status without ref' do
+ let(:commit_status) { create(:generic_commit_status, status: 'success') }
+
+ before { mr_merge_if_green_enabled }
+
+ it "doesn't merge a requests for status on other branch" do
+ allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([])
+
+ expect(MergeWorker).to_not receive(:perform_async)
+ service.trigger(commit_status)
+ end
+
+ it 'discovers branches and merges all merge requests when status is success' do
+ allow(project.repository).to receive(:branch_names_contains).
+ with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch])
+ allow(ci_commit).to receive(:success?).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
+ allow(ci_commit).to receive(:success?).and_return(true)
+
+ expect(MergeWorker).to receive(:perform_async)
+ service.trigger(commit_status)
+ end
+ end
- expect(MergeWorker).to receive(:perform_async)
- service.trigger(build)
+ context 'properly handles multiple stages' do
+ let(:ref) { mr_merge_if_green_enabled.source_branch }
+ let(:build) { create(:ci_build, commit: ci_commit, ref: ref, name: 'build', stage: 'build') }
+ let(:test) { create(:ci_build, commit: ci_commit, ref: ref, name: 'test', stage: 'test') }
+
+ before do
+ # This behavior of MergeRequest: we instantiate a new object
+ allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_wrap_original do
+ Ci::Commit.find(ci_commit.id)
+ end
+
+ # We create test after the build
+ allow(ci_commit).to receive(:create_next_builds).and_wrap_original do
+ test
+ end
+ end
+
+ it "doesn't merge if some stages failed" do
+ expect(MergeWorker).to_not receive(:perform_async)
+ build.success
+ test.drop
+ end
+
+ it 'merge when all stages succeeded' do
+ expect(MergeWorker).to receive(:perform_async)
+ build.success
+ test.success
+ end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 450250ba032..fea8182bd30 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -79,7 +79,7 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.notes.last.note).to include('changed to merged') }
it { expect(@merge_request).to be_merged }
- it { expect(@merge_request.diffs.length).to be > 0 }
+ it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 2e9e6e0870d..cb8cff2fa8c 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -7,6 +7,7 @@ describe MergeRequests::UpdateService, services: true do
let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) }
let(:project) { merge_request.project }
let(:label) { create(:label) }
+ let(:label2) { create(:label) }
before do
project.team << [user, :master]
@@ -53,7 +54,7 @@ describe MergeRequests::UpdateService, services: true do
it { expect(@merge_request.assignee).to eq(user2) }
it { expect(@merge_request).to be_closed }
it { expect(@merge_request.labels.count).to eq(1) }
- it { expect(@merge_request.labels.first.title).to eq('Bug') }
+ it { expect(@merge_request.labels.first.title).to eq(label.name) }
it { expect(@merge_request.target_branch).to eq('target') }
it 'should execute hooks with update action' do
@@ -98,6 +99,126 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'todos' do
+ let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
+
+ context 'when the title change' do
+ before do
+ update_merge_request({ title: 'New title' })
+ end
+
+ it 'marks pending todos as done' do
+ expect(pending_todo.reload).to be_done
+ end
+ end
+
+ context 'when the description change' do
+ before do
+ update_merge_request({ description: 'Also please fix' })
+ end
+
+ it 'marks pending todos as done' do
+ expect(pending_todo.reload).to be_done
+ end
+ end
+
+ context 'when is reassigned' do
+ before do
+ update_merge_request({ assignee: user2 })
+ end
+
+ it 'marks previous assignee pending todos as done' do
+ expect(pending_todo.reload).to be_done
+ end
+
+ it 'creates a pending todo for new assignee' do
+ attributes = {
+ project: project,
+ author: user,
+ user: user2,
+ target_id: merge_request.id,
+ target_type: merge_request.class.name,
+ action: Todo::ASSIGNED,
+ state: :pending
+ }
+
+ expect(Todo.where(attributes).count).to eq 1
+ end
+ end
+
+ context 'when the milestone change' do
+ before do
+ update_merge_request({ milestone: create(:milestone) })
+ end
+
+ it 'marks pending todos as done' do
+ expect(pending_todo.reload).to be_done
+ end
+ end
+
+ context 'when the labels change' do
+ before do
+ update_merge_request({ label_ids: [label.id] })
+ end
+
+ it 'marks pending todos as done' do
+ expect(pending_todo.reload).to be_done
+ end
+ end
+
+ context 'when the target branch change' do
+ before do
+ update_merge_request({ target_branch: 'target' })
+ end
+
+ it 'marks pending todos as done' do
+ expect(pending_todo.reload).to be_done
+ end
+ end
+ end
+
+ context 'when the issue is relabeled' do
+ let!(:non_subscriber) { create(:user) }
+ let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+
+ it 'sends notifications for subscribers of newly added labels' do
+ opts = { label_ids: [label.id] }
+
+ perform_enqueued_jobs do
+ @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+
+ context 'when issue has the `label` label' do
+ before { merge_request.labels << label }
+
+ it 'does not send notifications for existing labels' do
+ opts = { label_ids: [label.id, label2.id] }
+
+ perform_enqueued_jobs do
+ @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+ end
+
+ should_not_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+
+ it 'does not send notifications for removed labels' do
+ opts = { label_ids: [label2.id] }
+
+ perform_enqueued_jobs do
+ @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+ end
+
+ should_not_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+ end
+ end
+
context 'when MergeRequest has tasks' do
before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
@@ -130,6 +251,5 @@ describe MergeRequests::UpdateService, services: true do
end
end
end
-
end
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index a797a2fe4aa..ff23f13e1cb 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -14,9 +14,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
-
- expect(project).to receive(:execute_hooks)
- expect(project).to receive(:execute_services)
+
@note = Notes::CreateService.new(project, user, opts).execute
end
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
new file mode 100644
index 00000000000..d4c50f824c1
--- /dev/null
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Notes::PostProcessService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ describe :execute do
+ before do
+ project.team << [user, :master]
+ note_opts = {
+ note: 'Awesome comment',
+ noteable_type: 'Issue',
+ noteable_id: issue.id
+ }
+
+ @note = Notes::CreateService.new(project, user, note_opts).execute
+ end
+
+ it do
+ expect(project).to receive(:execute_hooks)
+ expect(project).to receive(:execute_services)
+
+ Notes::PostProcessService.new(@note).execute
+ end
+ end
+end
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
new file mode 100644
index 00000000000..dde4bde7dc2
--- /dev/null
+++ b/spec/services/notes/update_service_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Notes::UpdateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:note, project: project, noteable: issue, author: user, note: 'Old note') }
+
+ before do
+ project.team << [user, :master]
+ project.team << [user2, :developer]
+ end
+
+ describe '#execute' do
+ def update_note(opts)
+ @note = Notes::UpdateService.new(project, user, opts).execute(note)
+ @note.reload
+ end
+
+ context 'todos' do
+ let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
+
+ context 'when the note change' do
+ before do
+ update_note({ note: 'New note' })
+ end
+
+ it 'marks todos as done' do
+ expect(todo.reload).to be_done
+ end
+ end
+
+ context 'when the note does not change' do
+ before do
+ update_note({ note: 'Old note' })
+ end
+
+ it 'keep todos' do
+ expect(todo.reload).to be_pending
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index c103752198d..b5407397c1d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -52,6 +52,9 @@ describe NotificationService, services: true do
it do
add_users_with_subscription(note.project, issue)
+ # Ensure create SentNotification by noteable = issue 6 times, not noteable = note
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times
+
ActionMailer::Base.deliveries.clear
notification.new_note(note)
@@ -61,6 +64,7 @@ describe NotificationService, services: true do
should_email(note.noteable.assignee)
should_email(@u_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
should_not_email(note.author)
should_not_email(@u_participating)
@@ -220,10 +224,19 @@ describe NotificationService, services: true do
should_not_email(issue.assignee)
end
+
+ it "emails subscribers of the issue's labels" do
+ subscriber = create(:user)
+ label = create(:label, issues: [issue])
+ label.toggle_subscription(subscriber)
+ notification.new_issue(issue, @u_disabled)
+
+ should_email(subscriber)
+ end
end
describe :reassigned_issue do
- it 'should email new assignee' do
+ it 'emails new assignee' do
notification.reassigned_issue(issue, @u_disabled)
should_email(issue.assignee)
@@ -234,6 +247,91 @@ describe NotificationService, services: true do
should_not_email(@u_participating)
should_not_email(@u_disabled)
end
+
+ it 'emails previous assignee even if he has the "on mention" notif level' do
+ issue.update_attribute(:assignee, @u_mentioned)
+ issue.update_attributes(assignee: @u_watcher)
+ notification.reassigned_issue(issue, @u_disabled)
+
+ should_email(@u_mentioned)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ end
+
+ it 'emails new assignee even if he has the "on mention" notif level' do
+ issue.update_attributes(assignee: @u_mentioned)
+ notification.reassigned_issue(issue, @u_disabled)
+
+ expect(issue.assignee).to be @u_mentioned
+ should_email(issue.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ end
+
+ it 'emails new assignee' do
+ issue.update_attribute(:assignee, @u_mentioned)
+ notification.reassigned_issue(issue, @u_disabled)
+
+ expect(issue.assignee).to be @u_mentioned
+ should_email(issue.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ end
+
+ it 'does not email new assignee if they are the current user' do
+ issue.update_attribute(:assignee, @u_mentioned)
+ notification.reassigned_issue(issue, @u_mentioned)
+
+ expect(issue.assignee).to be @u_mentioned
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(issue.assignee)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ end
+ end
+
+ describe '#relabeled_issue' do
+ let(:label) { create(:label, issues: [issue]) }
+ let(:label2) { create(:label) }
+ let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
+
+ it "emails subscribers of the issue's added labels only" do
+ notification.relabeled_issue(issue, [label2], @u_disabled)
+
+ should_not_email(subscriber_to_label)
+ should_email(subscriber_to_label2)
+ end
+
+ it "doesn't send email to anyone but subscribers of the given labels" do
+ notification.relabeled_issue(issue, [label2], @u_disabled)
+
+ should_not_email(issue.assignee)
+ should_not_email(issue.author)
+ should_not_email(@u_watcher)
+ should_not_email(@u_participant_mentioned)
+ should_not_email(@subscriber)
+ should_not_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(subscriber_to_label)
+ should_email(subscriber_to_label2)
+ end
end
describe :close_issue do
@@ -245,6 +343,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -260,6 +359,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
end
@@ -282,10 +382,20 @@ describe NotificationService, services: true do
should_email(merge_request.assignee)
should_email(@u_watcher)
+ should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
end
+
+ it "emails subscribers of the merge request's labels" do
+ subscriber = create(:user)
+ label = create(:label, merge_requests: [merge_request])
+ label.toggle_subscription(subscriber)
+ notification.new_merge_request(merge_request, @u_disabled)
+
+ should_email(subscriber)
+ end
end
describe :reassigned_merge_request do
@@ -296,12 +406,42 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
end
end
+ describe :relabel_merge_request do
+ let(:label) { create(:label, merge_requests: [merge_request]) }
+ let(:label2) { create(:label) }
+ let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
+
+ it "emails subscribers of the merge request's added labels only" do
+ notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
+
+ should_not_email(subscriber_to_label)
+ should_email(subscriber_to_label2)
+ end
+
+ it "doesn't send email to anyone but subscribers of the given labels" do
+ notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
+
+ should_not_email(merge_request.assignee)
+ should_not_email(merge_request.author)
+ should_not_email(@u_watcher)
+ should_not_email(@u_participant_mentioned)
+ should_not_email(@subscriber)
+ should_not_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(subscriber_to_label)
+ should_email(subscriber_to_label2)
+ end
+ end
+
describe :closed_merge_request do
it do
notification.close_mr(merge_request, @u_disabled)
@@ -310,6 +450,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -324,6 +465,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -338,6 +480,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -387,25 +530,17 @@ describe NotificationService, services: true do
@subscriber = create :user
@unsubscriber = create :user
@subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING)
+ @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH)
project.team << [@subscribed_participant, :master]
project.team << [@subscriber, :master]
project.team << [@unsubscriber, :master]
+ project.team << [@watcher_and_subscriber, :master]
issuable.subscriptions.create(user: @subscriber, subscribed: true)
issuable.subscriptions.create(user: @subscribed_participant, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, subscribed: false)
- end
-
- def sent_to_user?(user)
- ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
- end
-
- def should_email(user)
- expect(sent_to_user?(user)).to be_truthy
- end
-
- def should_not_email(user)
- expect(sent_to_user?(user)).to be_falsey
+ # Make the watcher a subscriber to detect dupes
+ issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true)
end
end
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/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 5d0b18558b1..e43903dbd3c 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -32,6 +32,7 @@ describe Projects::CreateService, services: true do
it { expect(@project).to be_valid }
it { expect(@project.owner).to eq(@user) }
+ it { expect(@project.team.masters).to include(@user) }
it { expect(@project.namespace).to eq(@user.namespace) }
end
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index 5ceed5af9a5..f252e2c5902 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -33,12 +33,12 @@ describe Projects::DownloadService, services: true do
@link_to_file = download_file(@project, url)
end
- it { expect(@link_to_file).to have_key('alt') }
- it { expect(@link_to_file).to have_key('url') }
- it { expect(@link_to_file).to have_key('is_image') }
- it { expect(@link_to_file['is_image']).to be true }
- it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
- it { expect(@link_to_file['alt']).to eq('rails_sample') }
+ it { expect(@link_to_file).to have_key(:alt) }
+ it { expect(@link_to_file).to have_key(:url) }
+ it { expect(@link_to_file).to have_key(:is_image) }
+ it { expect(@link_to_file[:is_image]).to be true }
+ it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
+ it { expect(@link_to_file[:alt]).to eq('rails_sample') }
end
context 'a txt file' do
@@ -47,12 +47,12 @@ describe Projects::DownloadService, services: true do
@link_to_file = download_file(@project, url)
end
- it { expect(@link_to_file).to have_key('alt') }
- it { expect(@link_to_file).to have_key('url') }
- it { expect(@link_to_file).to have_key('is_image') }
- it { expect(@link_to_file['is_image']).to be false }
- it { expect(@link_to_file['url']).to match('doc_sample.txt') }
- it { expect(@link_to_file['alt']).to eq('doc_sample.txt') }
+ it { expect(@link_to_file).to have_key(:alt) }
+ it { expect(@link_to_file).to have_key(:url) }
+ it { expect(@link_to_file).to have_key(:is_image) }
+ it { expect(@link_to_file[:is_image]).to be false }
+ it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
+ it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') }
end
end
end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
new file mode 100644
index 00000000000..93bf1b81fbe
--- /dev/null
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Projects::HousekeepingService do
+ subject { Projects::HousekeepingService.new(project) }
+ let(:project) { create :project }
+
+ describe 'execute' do
+ before do
+ project.pushes_since_gc = 3
+ project.save!
+ end
+
+ 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)
+
+ subject.execute
+ expect(project.pushes_since_gc).to eq(0)
+ end
+
+ 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 { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
+ expect(project.pushes_since_gc).to eq(0)
+ end
+ end
+
+ describe 'needed?' do
+ it 'when the count is low enough' do
+ expect(subject.needed?).to eq(false)
+ end
+
+ it 'when the count is high enough' do
+ allow(project).to receive(:pushes_since_gc).and_return(10)
+ expect(subject.needed?).to eq(true)
+ end
+ end
+
+ describe 'increment!' do
+ it 'increments the pushes_since_gc counter' do
+ expect(project.pushes_since_gc).to eq(0)
+ subject.increment!
+ expect(project.pushes_since_gc).to eq(1)
+ end
+ end
+end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
new file mode 100644
index 00000000000..04f474c736c
--- /dev/null
+++ b/spec/services/projects/import_service_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe Projects::ImportService, services: true do
+ let!(:project) { create(:empty_project) }
+ let(:user) { project.creator }
+
+ subject { described_class.new(project, user) }
+
+ describe '#execute' do
+ context 'with unknown url' do
+ before do
+ project.import_url = Project::UNKNOWN_IMPORT_URL
+ end
+
+ it 'succeeds if repository is created successfully' do
+ expect(project).to receive(:create_repository).and_return(true)
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :success
+ end
+
+ it 'fails if repository creation fails' do
+ expect(project).to receive(:create_repository).and_return(false)
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'The repository could not be created.'
+ end
+ end
+
+ context 'with known url' do
+ before do
+ project.import_url = 'https://github.com/vim/vim.git'
+ end
+
+ it 'succeeds if repository import is successfully' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true)
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :success
+ end
+
+ it 'fails if repository import fails' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'Failed to import the repository'
+ end
+ end
+
+ context 'with valid importer' do
+ before do
+ stub_github_omniauth_provider
+
+ project.import_url = 'https://github.com/vim/vim.git'
+ project.import_type = 'github'
+
+ allow(project).to receive(:import_data).and_return(double.as_null_object)
+ end
+
+ it 'succeeds if importer succeeds' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true)
+ expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :success
+ end
+
+ it 'fails if importer fails' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true)
+ expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false)
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'The remote data could not be imported.'
+ end
+
+ it 'fails if importer raise an error' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true)
+ expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API'))
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'Github: failed to connect API'
+ end
+ end
+
+ def stub_github_omniauth_provider
+ provider = OpenStruct.new(
+ name: 'github',
+ app_id: 'asd123',
+ app_secret: 'asd123'
+ )
+
+ Gitlab.config.omniauth.providers << provider
+ end
+ end
+end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 3c06a890163..e8b9e6b9238 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -102,8 +102,8 @@ describe Projects::UpdateService, services: true do
describe :visibility_level do
let(:user) { create :user, admin: true }
- let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
- let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
+ let(:project) { create(:project, :internal) }
+ let(:forked_project) { create(:forked_project_with_submodules, :internal) }
let(:opts) { {} }
before do
diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb
new file mode 100644
index 00000000000..ce7d1455975
--- /dev/null
+++ b/spec/services/repair_ldap_blocked_user_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe RepairLdapBlockedUserService, services: true do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
+ let(:identity) { user.ldap_identity }
+ subject(:service) { RepairLdapBlockedUserService.new(user) }
+
+ describe '#execute' do
+ it 'change to normal block after destroying last ldap identity' do
+ identity.destroy
+ service.execute
+
+ expect(user.reload).not_to be_ldap_blocked
+ end
+
+ it 'change to normal block after changing last ldap identity to another provider' do
+ identity.update_attribute(:provider, 'twitter')
+ service.execute
+
+ expect(user.reload).not_to be_ldap_blocked
+ end
+ end
+end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index febc78d2784..fef211ded50 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -9,37 +9,54 @@ describe SystemHooksService, services: true do
let(:group_member) { create(:group_member) }
context 'event data' do
- it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :email, :user_id) }
- it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) }
- it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
- it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
+ it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) }
+ it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) }
+ it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
+ it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
it { expect(event_data(key, :create)).to include(:username, :key, :id) }
it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
it do
+ project.old_path_with_namespace = 'renamed_from_path'
+ expect(event_data(project, :rename)).to include(
+ :event_name, :name, :created_at, :updated_at, :path, :project_id,
+ :owner_name, :owner_email, :project_visibility,
+ :old_path_with_namespace
+ )
+ end
+ it do
+ project.old_path_with_namespace = 'transfered_from_path'
+ expect(event_data(project, :transfer)).to include(
+ :event_name, :name, :created_at, :updated_at, :path, :project_id,
+ :owner_name, :owner_email, :project_visibility,
+ :old_path_with_namespace
+ )
+ end
+
+ it do
expect(event_data(group, :create)).to include(
- :event_name, :name, :created_at, :path, :group_id, :owner_name,
- :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id,
+ :owner_name, :owner_email
)
end
it do
expect(event_data(group, :destroy)).to include(
- :event_name, :name, :created_at, :path, :group_id, :owner_name,
- :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id,
+ :owner_name, :owner_email
)
end
it do
expect(event_data(group_member, :create)).to include(
- :event_name, :created_at, :group_name, :group_path, :group_id, :user_id,
- :user_name, :user_email, :group_access
+ :event_name, :created_at, :updated_at, :group_name, :group_path,
+ :group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
it do
expect(event_data(group_member, :destroy)).to include(
- :event_name, :created_at, :group_name, :group_path, :group_id, :user_id,
- :user_name, :user_email, :group_access
+ :event_name, :created_at, :updated_at, :group_name, :group_path,
+ :group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
end
@@ -49,6 +66,8 @@ describe SystemHooksService, services: true do
it { expect(event_name(user, :destroy)).to eq "user_destroy" }
it { expect(event_name(project, :create)).to eq "project_create" }
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
+ it { expect(event_name(project, :rename)).to eq "project_rename" }
+ it { expect(event_name(project, :transfer)).to eq "project_transfer" }
it { expect(event_name(project_member, :create)).to eq "user_add_to_team" }
it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" }
it { expect(event_name(key, :create)).to eq 'key_create' }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index c9f828ae2f7..8e6292014d4 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -171,7 +171,7 @@ describe SystemNoteService, services: true do
context 'when milestone added' do
it 'sets the note text' do
- expect(subject.note).to eq "Milestone changed to #{milestone.title}"
+ expect(subject.note).to eq "Milestone changed to #{milestone.to_reference}"
end
end
@@ -280,6 +280,18 @@ describe SystemNoteService, services: true do
end
end
+ describe '.new_issue_branch' do
+ subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") }
+
+ it_behaves_like 'a system note'
+
+ context 'when a branch is created from the new branch button' do
+ it 'sets the note text' do
+ expect(subject.note).to match /\AStarted branch [`1-mepmep`]/
+ end
+ end
+ end
+
describe '.cross_reference' do
subject { described_class.cross_reference(noteable, mentioner, author) }
@@ -424,6 +436,21 @@ describe SystemNoteService, services: true do
to be_falsey
end
end
+
+ context 'commit with cross-reference from fork' do
+ let(:author2) { create(:user) }
+ let(:forked_project) { Projects::ForkService.new(project, author2).execute }
+ let(:commit2) { forked_project.commit }
+
+ before do
+ described_class.cross_reference(noteable, commit0, author2)
+ end
+
+ it 'is true when a fork mentions an external issue' do
+ expect(described_class.cross_reference_exists?(noteable, commit2)).
+ to be true
+ end
+ end
end
include JiraServiceHelper
@@ -459,8 +486,8 @@ describe SystemNoteService, services: true do
describe "existing reference" do
before do
- message = "[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]."
- WebMock.stub_request(:get, jira_api_comment_url).to_return(body: "{\"comments\":[{\"body\":\"#{message}\"}]}")
+ message = %Q{[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
+ WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]}))
end
subject { described_class.cross_reference(jira_issue, commit, author) }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
new file mode 100644
index 00000000000..96420acb31d
--- /dev/null
+++ b/spec/services/todo_service_spec.rb
@@ -0,0 +1,274 @@
+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(:project) { create(:project) }
+ let(:mentions) { [author.to_reference, john_doe.to_reference, michael.to_reference, stranger.to_reference].join(' ') }
+ let(:service) { described_class.new }
+
+ before do
+ project.team << [author, :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) }
+
+ describe '#new_issue' do
+ it 'creates a todo if assigned' do
+ service.new_issue(issue, author)
+
+ should_create_todo(user: john_doe, target: issue, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo if unassigned' do
+ should_not_create_any_todo { service.new_issue(unassigned_issue, author) }
+ end
+
+ it 'does not create a todo if assignee is the current user' do
+ should_not_create_any_todo { service.new_issue(unassigned_issue, john_doe) }
+ end
+
+ 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_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)
+ end
+ end
+
+ describe '#update_issue' 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: 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)
+ end
+
+ it 'does not create a todo if user was already mentioned' do
+ create(:todo, :mentioned, user: michael, project: project, target: issue, author: author)
+
+ expect { service.update_issue(issue, author) }.not_to change(michael.todos, :count)
+ end
+ end
+
+ describe '#close_issue' do
+ it 'marks related pending todos to the target for the user as done' do
+ first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+ second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+
+ service.close_issue(issue, john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+ end
+
+ describe '#reassigned_issue' do
+ it 'creates a pending todo for new assignee' do
+ unassigned_issue.update_attribute(:assignee, john_doe)
+ service.reassigned_issue(unassigned_issue, author)
+
+ should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo if unassigned' do
+ issue.update_attribute(:assignee, nil)
+
+ should_not_create_any_todo { service.reassigned_issue(issue, author) }
+ end
+
+ it 'does not create a todo if new assignee is the current user' do
+ unassigned_issue.update_attribute(:assignee, john_doe)
+
+ should_not_create_any_todo { service.reassigned_issue(unassigned_issue, john_doe) }
+ end
+ end
+
+ describe '#mark_pending_todos_as_done' do
+ it 'marks related pending todos to the target for the user as done' do
+ first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+ second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+
+ service.mark_pending_todos_as_done(issue, john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+ end
+
+ 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(: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_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) }
+
+ it 'mark related pending todos to the noteable for the note author as done' do
+ first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+ second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+
+ service.new_note(note, john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+
+ it 'mark related pending todos to the noteable for the award note author as done' do
+ service.new_note(award_note, john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+
+ it 'does not mark related pending todos it is a system note' do
+ service.new_note(system_note, john_doe)
+
+ expect(first_todo.reload).to be_pending
+ expect(second_todo.reload).to be_pending
+ end
+
+ 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: 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)
+ 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) }
+ end
+
+ it 'does not create todo when leaving a note on snippet' do
+ should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
+ end
+ end
+ end
+
+ describe 'Merge Requests' do
+ let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: mentions) }
+ let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
+
+ describe '#new_merge_request' do
+ it 'creates a pending todo if assigned' do
+ service.new_merge_request(mr_assigned, author)
+
+ should_create_todo(user: john_doe, target: mr_assigned, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo if unassigned' do
+ should_not_create_any_todo { service.new_merge_request(mr_unassigned, author) }
+ end
+
+ it 'does not create a todo if assignee is the current user' do
+ should_not_create_any_todo { service.new_merge_request(mr_unassigned, john_doe) }
+ end
+
+ 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_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)
+ end
+ end
+
+ describe '#update_merge_request' 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: 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)
+ 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)
+
+ expect { service.update_merge_request(mr_assigned, author) }.not_to change(michael.todos, :count)
+ end
+ end
+
+ describe '#close_merge_request' do
+ it 'marks related pending todos to the target for the user as done' do
+ first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author)
+ second_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author)
+ service.close_merge_request(mr_assigned, john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+ end
+
+ describe '#reassigned_merge_request' do
+ it 'creates a pending todo for new assignee' do
+ mr_unassigned.update_attribute(:assignee, john_doe)
+ service.reassigned_merge_request(mr_unassigned, author)
+
+ should_create_todo(user: john_doe, target: mr_unassigned, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo if unassigned' do
+ mr_assigned.update_attribute(:assignee, nil)
+
+ should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) }
+ end
+
+ it 'does not create a todo if new assignee is the current user' do
+ mr_assigned.update_attribute(:assignee, john_doe)
+
+ should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, john_doe) }
+ end
+ end
+
+ describe '#merge_merge_request' do
+ it 'marks related pending todos to the target for the user as done' do
+ first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author)
+ second_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author)
+ service.merge_merge_request(mr_assigned, john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+ end
+ end
+
+ def should_create_todo(attributes = {})
+ attributes.reverse_merge!(
+ project: project,
+ author: author,
+ state: :pending
+ )
+
+ expect(Todo.where(attributes).count).to eq 1
+ end
+
+ def should_not_create_todo(attributes = {})
+ attributes.reverse_merge!(
+ project: project,
+ author: author,
+ state: :pending
+ )
+
+ expect(Todo.where(attributes).count).to eq 0
+ end
+
+ def should_not_create_any_todo
+ expect { yield }.not_to change(Todo, :count)
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 0225a0ee53f..596d607f2a1 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -14,7 +14,7 @@ require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'sidekiq/testing/inline'
-require 'benchmark/ips'
+require 'rspec/retry'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
@@ -25,16 +25,19 @@ RSpec.configure do |config|
config.use_instantiated_fixtures = false
config.mock_with :rspec
+ config.verbose_retry = true
+ config.display_try_failure_messages = true
+
config.include Devise::TestHelpers, type: :controller
config.include LoginHelpers, type: :feature
config.include LoginHelpers, type: :request
config.include StubConfiguration
+ config.include EmailHelpers
config.include RelativeUrl, type: feature
config.include TestEnv
config.include ActiveJob::TestHelper
config.include StubGitlabCalls
config.include StubGitlabData
- config.include BenchmarkMatchers, benchmark: true
config.infer_spec_type_from_file_location!
config.raise_errors_for_deprecations!
@@ -48,4 +51,10 @@ FactoryGirl::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
+# Work around a Rails 4.2.5.1 issue
+# See https://github.com/rspec/rspec-rails/issues/1532
+RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator.class_eval do
+ alias_method :find_all_anywhere, :find_all
+end
+
ActiveRecord::Migration.maintain_test_schema!
diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb
new file mode 100644
index 00000000000..352a6eeec79
--- /dev/null
+++ b/spec/support/api/pagination_shared_examples.rb
@@ -0,0 +1,20 @@
+# Specs for paginated resources.
+#
+# Requires an API request:
+# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
+shared_examples 'a paginated resources' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'has pagination headers' do
+ expect(response.headers).to include('X-Total')
+ expect(response.headers).to include('X-Total-Pages')
+ expect(response.headers).to include('X-Per-Page')
+ expect(response.headers).to include('X-Page')
+ expect(response.headers).to include('X-Next-Page')
+ expect(response.headers).to include('X-Prev-Page')
+ expect(response.headers).to include('Link')
+ end
+end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index fed1ab6ee33..e1f90e17cce 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,10 +7,10 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout)
+ Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
end
-Capybara.default_wait_time = timeout
+Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = true
unless ENV['CI'] || ENV['CI_SERVER']
@@ -19,3 +19,9 @@ unless ENV['CI'] || ENV['CI_SERVER']
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
end
+
+RSpec.configure do |config|
+ config.before(:suite) do
+ TestEnv.warm_asset_cache
+ end
+end
diff --git a/spec/support/email_format_shared_examples.rb b/spec/support/email_format_shared_examples.rb
new file mode 100644
index 00000000000..b924a208e71
--- /dev/null
+++ b/spec/support/email_format_shared_examples.rb
@@ -0,0 +1,44 @@
+# Specifications for behavior common to all objects with an email attribute.
+# Takes a list of email-format attributes and requires:
+# - subject { "the object with a attribute= setter" }
+# Note: You have access to `email_value` which is the email address value
+# being currently tested).
+
+shared_examples 'an object with email-formated attributes' do |*attributes|
+ attributes.each do |attribute|
+ describe "specifically its :#{attribute} attribute" do
+ %w[
+ info@example.com
+ info+test@example.com
+ o'reilly@example.com
+ mailto:test@example.com
+ lol!'+=?><#$%^&*()@gmail.com
+ ].each do |valid_email|
+ context "with a value of '#{valid_email}'" do
+ let(:email_value) { valid_email }
+
+ it 'is valid' do
+ subject.send("#{attribute}=", valid_email)
+
+ expect(subject).to be_valid
+ end
+ end
+ end
+
+ %w[
+ foobar
+ test@test@example.com
+ ].each do |invalid_email|
+ context "with a value of '#{invalid_email}'" do
+ let(:email_value) { invalid_email }
+
+ it 'is invalid' do
+ subject.send("#{attribute}=", invalid_email)
+
+ expect(subject).to be_invalid
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
new file mode 100644
index 00000000000..a85ab22ce36
--- /dev/null
+++ b/spec/support/email_helpers.rb
@@ -0,0 +1,13 @@
+module EmailHelpers
+ def sent_to_user?(user)
+ ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+ end
+
+ def should_email(user)
+ expect(sent_to_user?(user)).to be_truthy
+ end
+
+ def should_not_email(user)
+ expect(sent_to_user?(user)).to be_falsey
+ end
+end
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index d6e03cbef3d..ef5ea7d626e 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -67,9 +67,9 @@ module FilterSpecHelper
if reference =~ /\A(.+)?.\d+\z/
# Integer-based reference with optional project prefix
reference.gsub(/\d+\z/) { |i| i.to_i + 1 }
- elsif reference =~ /\A(.+@)?(\h{6,40}\z)/
+ elsif reference =~ /\A(.+@)?(\h{7,40}\z)/
# SHA-based reference with optional prefix
- reference.gsub(/\h{6,40}\z/) { |v| v.reverse }
+ reference.gsub(/\h{7,40}\z/) { |v| v.reverse }
else
reference.gsub(/\w+\z/) { |v| v.reverse }
end
diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml
index 3482145404e..a5b256bd3ec 100644
--- a/spec/support/gitlab_stubs/gitlab_ci.yml
+++ b/spec/support/gitlab_stubs/gitlab_ci.yml
@@ -36,8 +36,8 @@ staging:
script: "cap deploy stating"
type: deploy
tags:
- - capistrano
- - debian
+ - ruby
+ - mysql
except:
- stable
@@ -47,8 +47,8 @@ production:
- cap deploy production
- cap notify
tags:
- - capistrano
- - debian
+ - ruby
+ - mysql
only:
- master
- /^deploy-.*$/
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index d6d3062a197..73c6792b65f 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -28,6 +28,10 @@ class MarkdownFeature
end
end
+ def project_wiki
+ @project_wiki ||= ProjectWiki.new(project, user)
+ end
+
def issue
@issue ||= create(:issue, project: project)
end
@@ -59,6 +63,10 @@ class MarkdownFeature
@label ||= create(:label, name: 'awaiting feedback', project: project)
end
+ def milestone
+ @milestone ||= create(:milestone, project: project)
+ end
+
# Cross-references -----------------------------------------------------------
def xproject
@@ -93,6 +101,10 @@ class MarkdownFeature
end
end
+ def xmilestone
+ @xmilestone ||= create(:milestone, project: xproject)
+ end
+
def urls
Gitlab::Application.routes.url_helpers
end
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index 558e8b1612f..4e007c777e3 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -15,6 +15,8 @@ module AccessMatchers
logout
when :admin
login_as(create(:admin))
+ when :external
+ login_as(create(:user, external: true))
when User
login_as(user)
else
diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb
deleted file mode 100644
index 84f655c2119..00000000000
--- a/spec/support/matchers/benchmark_matchers.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-module BenchmarkMatchers
- extend RSpec::Matchers::DSL
-
- def self.included(into)
- into.extend(ClassMethods)
- end
-
- matcher :iterate_per_second do |min_iterations|
- supports_block_expectations
-
- match do |block|
- @max_stddev ||= 30
-
- @entry = benchmark(&block)
-
- expect(@entry.ips).to be >= min_iterations
- expect(@entry.stddev_percentage).to be <= @max_stddev
- end
-
- chain :with_maximum_stddev do |value|
- @max_stddev = value
- end
-
- description do
- "run at least #{min_iterations} iterations per second"
- end
-
- failure_message do
- ips = @entry.ips.round(2)
- stddev = @entry.stddev_percentage.round(2)
-
- "expected at least #{min_iterations} iterations per second " \
- "with a maximum stddev of #{@max_stddev}%, instead of " \
- "#{ips} iterations per second with a stddev of #{stddev}%"
- end
- end
-
- # Benchmarks the given block and returns a Benchmark::IPS::Report::Entry.
- def benchmark(&block)
- report = Benchmark.ips(quiet: true) do |bench|
- bench.report do
- instance_eval(&block)
- end
- end
-
- report.entries[0]
- end
-
- module ClassMethods
- # Wraps around rspec's subject method so you can write:
- #
- # benchmark_subject { SomeClass.some_method }
- #
- # instead of:
- #
- # subject { -> { SomeClass.some_method } }
- def benchmark_subject(&block)
- subject { block }
- end
- end
-end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 7eadcd58c1f..1d52489e804 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -66,6 +66,24 @@ module MarkdownMatchers
end
end
+ # GollumTagsFilter
+ matcher :parse_gollum_tags do
+ def have_image(src)
+ have_css("img[src$='#{src}']")
+ end
+
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_link('linked-resource', href: 'linked-resource')
+ expect(actual).to have_link('link-text', href: 'linked-resource')
+ expect(actual).to have_link('http://example.com', href: 'http://example.com')
+ expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf')
+ expect(actual).to have_image('/gitlabhq/wikis/images/example.jpg')
+ expect(actual).to have_image('http://example.com/images/example.jpg')
+ end
+ end
+
# UserReferenceFilter
matcher :reference_users do
set_default_markdown_messages
@@ -130,6 +148,15 @@ module MarkdownMatchers
end
end
+ # MilestoneReferenceFilter
+ matcher :reference_milestones do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_selector('a.gfm.gfm-milestone', count: 3)
+ end
+ end
+
# TaskListFilter
matcher :parse_task_lists do
set_default_markdown_messages
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/support/project_hook_data_shared_example.rb b/spec/support/project_hook_data_shared_example.rb
new file mode 100644
index 00000000000..422083875d7
--- /dev/null
+++ b/spec/support/project_hook_data_shared_example.rb
@@ -0,0 +1,27 @@
+RSpec.shared_examples 'project hook data' do |project_key: :project|
+ it 'contains project data' do
+ expect(data[project_key][:name]).to eq(project.name)
+ expect(data[project_key][:description]).to eq(project.description)
+ expect(data[project_key][:web_url]).to eq(project.web_url)
+ expect(data[project_key][:avatar_url]).to eq(project.avatar_url)
+ expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo)
+ expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo)
+ expect(data[project_key][:namespace]).to eq(project.namespace.name)
+ expect(data[project_key][:visibility_level]).to eq(project.visibility_level)
+ expect(data[project_key][:path_with_namespace]).to eq(project.path_with_namespace)
+ expect(data[project_key][:default_branch]).to eq(project.default_branch)
+ expect(data[project_key][:homepage]).to eq(project.web_url)
+ expect(data[project_key][:url]).to eq(project.url_to_repo)
+ expect(data[project_key][:ssh_url]).to eq(project.ssh_url_to_repo)
+ expect(data[project_key][:http_url]).to eq(project.http_url_to_repo)
+ end
+end
+
+RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project|
+ it 'contains deprecated repository data' do
+ expect(data[:repository][:name]).to eq(project.name)
+ expect(data[:repository][:description]).to eq(project.description)
+ expect(data[:repository][:url]).to eq(project.url_to_repo)
+ expect(data[:repository][:homepage]).to eq(project.web_url)
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 4f4743bff6d..0d1bd030f3c 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -146,6 +146,22 @@ module TestEnv
FileUtils.chmod_R 0755, target_repo_path
end
+ # When no cached assets exist, manually hit the root path to create them
+ #
+ # Otherwise they'd be created by the first test, often timing out and
+ # causing a transient test failure
+ def warm_asset_cache
+ return if warm_asset_cache?
+ return unless defined?(Capybara)
+
+ Capybara.current_session.driver.visit '/'
+ end
+
+ def warm_asset_cache?
+ cache = Rails.root.join(*%w(tmp cache assets test))
+ Dir.exist?(cache) && Dir.entries(cache).length > 2
+ end
+
private
def factory_repo_path
@@ -172,7 +188,6 @@ module TestEnv
'gitlab-test-fork'
end
-
# Prevent developer git configurations from being persisted to test
# repositories
def git_env
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
index 692d219e9f1..b90fc112671 100644
--- a/spec/support/wait_for_ajax.rb
+++ b/spec/support/wait_for_ajax.rb
@@ -1,6 +1,6 @@
module WaitForAjax
def wait_for_ajax
- Timeout.timeout(Capybara.default_wait_time) do
+ Timeout.timeout(Capybara.default_max_wait_time) do
loop until finished_all_ajax_requests?
end
end
diff --git a/spec/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb
new file mode 100644
index 00000000000..107b6e30924
--- /dev/null
+++ b/spec/support/workhorse_helpers.rb
@@ -0,0 +1,16 @@
+module WorkhorseHelpers
+ extend self
+
+ def workhorse_send_data
+ @_workhorse_send_data ||= begin
+ header = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]
+ split_header = header.split(':')
+ type = split_header.shift
+ header = split_header.join(':')
+ [
+ type,
+ JSON.parse(Base64.urlsafe_decode64(header)),
+ ]
+ end
+ end
+end
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
new file mode 100644
index 00000000000..05a76ee4bdb
--- /dev/null
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+describe 'devise/shared/_signin_box' do
+ describe 'Crowd form' do
+ before do
+ stub_devise
+ assign(:ldap_servers, [])
+ end
+
+ it 'is shown when Crowd is enabled' do
+ enable_crowd
+
+ render
+
+ expect(rendered).to have_selector('#tab-crowd form')
+ end
+
+ it 'is not shown when Crowd is disabled' do
+ render
+
+ expect(rendered).not_to have_selector('#tab-crowd')
+ end
+ end
+
+ def stub_devise
+ allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
+ allow(view).to receive(:resource).and_return(spy)
+ allow(view).to receive(:resource_name).and_return(:user)
+ end
+
+ def enable_crowd
+ allow(view).to receive(:form_based_providers).and_return([:crowd])
+ allow(view).to receive(:crowd_enabled?).and_return(true)
+ allow(view).to receive(:user_omniauth_authorize_path).with('crowd').
+ and_return('/crowd')
+ end
+end
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
new file mode 100644
index 00000000000..14c56521280
--- /dev/null
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe DeleteUserWorker do
+ let!(:user) { create(:user) }
+ let!(:current_user) { create(:user) }
+
+ it "calls the DeleteUserWorker with the params it was given" do
+ expect_any_instance_of(DeleteUserService).to receive(:execute).
+ with(user, {})
+
+ DeleteUserWorker.new.perform(current_user.id, user.id)
+ end
+
+ it "uses symbolized keys" do
+ expect_any_instance_of(DeleteUserService).to receive(:execute).
+ with(user, test: "test")
+
+ DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test")
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index e4151b9bb6a..0265dbe9c66 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -11,7 +11,7 @@ describe PostReceive do
end
end
- context "web hook" do
+ context "webhook" do
let(:project) { create(:project) }
let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id }
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index dae31992620..172537474ee 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -19,6 +19,18 @@ describe RepositoryForkWorker do
fork_project.namespace.path)
end
+ it 'flushes the empty caches' do
+ expect_any_instance_of(Gitlab::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
+
+ 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)
subject.perform(
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
new file mode 100644
index 00000000000..6739063543b
--- /dev/null
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe RepositoryImportWorker do
+ let(:project) { create(:project) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'imports a project' do
+ expect_any_instance_of(Projects::ImportService).to receive(:execute).
+ and_return({ status: :ok })
+
+ expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
+ expect_any_instance_of(Project).to receive(:import_finish)
+
+ subject.perform(project.id)
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js
new file mode 100755
index 00000000000..c264262ba73
--- /dev/null
+++ b/vendor/assets/javascripts/Chart.js
@@ -0,0 +1,3477 @@
+/*!
+ * Chart.js
+ * http://chartjs.org/
+ * Version: 1.0.2
+ *
+ * Copyright 2015 Nick Downie
+ * Released under the MIT license
+ * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
+ */
+
+
+(function(){
+
+ "use strict";
+
+ //Declare root variable - window in the browser, global on the server
+ var root = this,
+ previous = root.Chart;
+
+ //Occupy the global variable of Chart, and create a simple base class
+ var Chart = function(context){
+ var chart = this;
+ this.canvas = context.canvas;
+
+ this.ctx = context;
+
+ //Variables global to the chart
+ var computeDimension = function(element,dimension)
+ {
+ if (element['offset'+dimension])
+ {
+ return element['offset'+dimension];
+ }
+ else
+ {
+ return document.defaultView.getComputedStyle(element).getPropertyValue(dimension);
+ }
+ }
+
+ var width = this.width = computeDimension(context.canvas,'Width');
+ var height = this.height = computeDimension(context.canvas,'Height');
+
+ // Firefox requires this to work correctly
+ context.canvas.width = width;
+ context.canvas.height = height;
+
+ var width = this.width = context.canvas.width;
+ var height = this.height = context.canvas.height;
+ this.aspectRatio = this.width / this.height;
+ //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
+ helpers.retinaScale(this);
+
+ return this;
+ };
+ //Globally expose the defaults to allow for user updating/changing
+ Chart.defaults = {
+ global: {
+ // Boolean - Whether to animate the chart
+ animation: true,
+
+ // Number - Number of animation steps
+ animationSteps: 60,
+
+ // String - Animation easing effect
+ animationEasing: "easeOutQuart",
+
+ // Boolean - If we should show the scale at all
+ showScale: true,
+
+ // Boolean - If we want to override with a hard coded scale
+ scaleOverride: false,
+
+ // ** Required if scaleOverride is true **
+ // Number - The number of steps in a hard coded scale
+ scaleSteps: null,
+ // Number - The value jump in the hard coded scale
+ scaleStepWidth: null,
+ // Number - The scale starting value
+ scaleStartValue: null,
+
+ // String - Colour of the scale line
+ scaleLineColor: "rgba(0,0,0,.1)",
+
+ // Number - Pixel width of the scale line
+ scaleLineWidth: 1,
+
+ // Boolean - Whether to show labels on the scale
+ scaleShowLabels: true,
+
+ // Interpolated JS string - can access value
+ scaleLabel: "<%=value%>",
+
+ // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there
+ scaleIntegersOnly: true,
+
+ // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
+ scaleBeginAtZero: false,
+
+ // String - Scale label font declaration for the scale label
+ scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+ // Number - Scale label font size in pixels
+ scaleFontSize: 12,
+
+ // String - Scale label font weight style
+ scaleFontStyle: "normal",
+
+ // String - Scale label font colour
+ scaleFontColor: "#666",
+
+ // Boolean - whether or not the chart should be responsive and resize when the browser does.
+ responsive: false,
+
+ // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
+ maintainAspectRatio: true,
+
+ // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove
+ showTooltips: true,
+
+ // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function
+ customTooltips: false,
+
+ // Array - Array of string names to attach tooltip events
+ tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"],
+
+ // String - Tooltip background colour
+ tooltipFillColor: "rgba(0,0,0,0.8)",
+
+ // String - Tooltip label font declaration for the scale label
+ tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+ // Number - Tooltip label font size in pixels
+ tooltipFontSize: 14,
+
+ // String - Tooltip font weight style
+ tooltipFontStyle: "normal",
+
+ // String - Tooltip label font colour
+ tooltipFontColor: "#fff",
+
+ // String - Tooltip title font declaration for the scale label
+ tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+ // Number - Tooltip title font size in pixels
+ tooltipTitleFontSize: 14,
+
+ // String - Tooltip title font weight style
+ tooltipTitleFontStyle: "bold",
+
+ // String - Tooltip title font colour
+ tooltipTitleFontColor: "#fff",
+
+ // Number - pixel width of padding around tooltip text
+ tooltipYPadding: 6,
+
+ // Number - pixel width of padding around tooltip text
+ tooltipXPadding: 6,
+
+ // Number - Size of the caret on the tooltip
+ tooltipCaretSize: 8,
+
+ // Number - Pixel radius of the tooltip border
+ tooltipCornerRadius: 6,
+
+ // Number - Pixel offset from point x to tooltip edge
+ tooltipXOffset: 10,
+
+ // String - Template string for single tooltips
+ tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
+
+ // String - Template string for single tooltips
+ multiTooltipTemplate: "<%= value %>",
+
+ // String - Colour behind the legend colour block
+ multiTooltipKeyBackground: '#fff',
+
+ // Function - Will fire on animation progression.
+ onAnimationProgress: function(){},
+
+ // Function - Will fire on animation completion.
+ onAnimationComplete: function(){}
+
+ }
+ };
+
+ //Create a dictionary of chart types, to allow for extension of existing types
+ Chart.types = {};
+
+ //Global Chart helpers object for utility methods and classes
+ var helpers = Chart.helpers = {};
+
+ //-- Basic js utility methods
+ var each = helpers.each = function(loopable,callback,self){
+ var additionalArgs = Array.prototype.slice.call(arguments, 3);
+ // Check to see if null or undefined firstly.
+ if (loopable){
+ if (loopable.length === +loopable.length){
+ var i;
+ for (i=0; i<loopable.length; i++){
+ callback.apply(self,[loopable[i], i].concat(additionalArgs));
+ }
+ }
+ else{
+ for (var item in loopable){
+ callback.apply(self,[loopable[item],item].concat(additionalArgs));
+ }
+ }
+ }
+ },
+ clone = helpers.clone = function(obj){
+ var objClone = {};
+ each(obj,function(value,key){
+ if (obj.hasOwnProperty(key)) objClone[key] = value;
+ });
+ return objClone;
+ },
+ extend = helpers.extend = function(base){
+ each(Array.prototype.slice.call(arguments,1), function(extensionObject) {
+ each(extensionObject,function(value,key){
+ if (extensionObject.hasOwnProperty(key)) base[key] = value;
+ });
+ });
+ return base;
+ },
+ merge = helpers.merge = function(base,master){
+ //Merge properties in left object over to a shallow clone of object right.
+ var args = Array.prototype.slice.call(arguments,0);
+ args.unshift({});
+ return extend.apply(null, args);
+ },
+ indexOf = helpers.indexOf = function(arrayToSearch, item){
+ if (Array.prototype.indexOf) {
+ return arrayToSearch.indexOf(item);
+ }
+ else{
+ for (var i = 0; i < arrayToSearch.length; i++) {
+ if (arrayToSearch[i] === item) return i;
+ }
+ return -1;
+ }
+ },
+ where = helpers.where = function(collection, filterCallback){
+ var filtered = [];
+
+ helpers.each(collection, function(item){
+ if (filterCallback(item)){
+ filtered.push(item);
+ }
+ });
+
+ return filtered;
+ },
+ findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){
+ // Default to start of the array
+ if (!startIndex){
+ startIndex = -1;
+ }
+ for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
+ var currentItem = arrayToSearch[i];
+ if (filterCallback(currentItem)){
+ return currentItem;
+ }
+ }
+ },
+ findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){
+ // Default to end of the array
+ if (!startIndex){
+ startIndex = arrayToSearch.length;
+ }
+ for (var i = startIndex - 1; i >= 0; i--) {
+ var currentItem = arrayToSearch[i];
+ if (filterCallback(currentItem)){
+ return currentItem;
+ }
+ }
+ },
+ inherits = helpers.inherits = function(extensions){
+ //Basic javascript inheritance based on the model created in Backbone.js
+ var parent = this;
+ var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); };
+
+ var Surrogate = function(){ this.constructor = ChartElement;};
+ Surrogate.prototype = parent.prototype;
+ ChartElement.prototype = new Surrogate();
+
+ ChartElement.extend = inherits;
+
+ if (extensions) extend(ChartElement.prototype, extensions);
+
+ ChartElement.__super__ = parent.prototype;
+
+ return ChartElement;
+ },
+ noop = helpers.noop = function(){},
+ uid = helpers.uid = (function(){
+ var id=0;
+ return function(){
+ return "chart-" + id++;
+ };
+ })(),
+ warn = helpers.warn = function(str){
+ //Method for warning of errors
+ if (window.console && typeof window.console.warn == "function") console.warn(str);
+ },
+ amd = helpers.amd = (typeof define == 'function' && define.amd),
+ //-- Math methods
+ isNumber = helpers.isNumber = function(n){
+ return !isNaN(parseFloat(n)) && isFinite(n);
+ },
+ max = helpers.max = function(array){
+ return Math.max.apply( Math, array );
+ },
+ min = helpers.min = function(array){
+ return Math.min.apply( Math, array );
+ },
+ cap = helpers.cap = function(valueToCap,maxValue,minValue){
+ if(isNumber(maxValue)) {
+ if( valueToCap > maxValue ) {
+ return maxValue;
+ }
+ }
+ else if(isNumber(minValue)){
+ if ( valueToCap < minValue ){
+ return minValue;
+ }
+ }
+ return valueToCap;
+ },
+ getDecimalPlaces = helpers.getDecimalPlaces = function(num){
+ if (num%1!==0 && isNumber(num)){
+ return num.toString().split(".")[1].length;
+ }
+ else {
+ return 0;
+ }
+ },
+ toRadians = helpers.radians = function(degrees){
+ return degrees * (Math.PI/180);
+ },
+ // Gets the angle from vertical upright to the point about a centre.
+ getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){
+ var distanceFromXCenter = anglePoint.x - centrePoint.x,
+ distanceFromYCenter = anglePoint.y - centrePoint.y,
+ radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
+
+
+ var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter);
+
+ //If the segment is in the top left quadrant, we need to add another rotation to the angle
+ if (distanceFromXCenter < 0 && distanceFromYCenter < 0){
+ angle += Math.PI*2;
+ }
+
+ return {
+ angle: angle,
+ distance: radialDistanceFromCenter
+ };
+ },
+ aliasPixel = helpers.aliasPixel = function(pixelWidth){
+ return (pixelWidth % 2 === 0) ? 0 : 0.5;
+ },
+ splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){
+ //Props to Rob Spencer at scaled innovation for his post on splining between points
+ //http://scaledinnovation.com/analytics/splines/aboutSplines.html
+ var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)),
+ d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)),
+ fa=t*d01/(d01+d12),// scaling factor for triangle Ta
+ fb=t*d12/(d01+d12);
+ return {
+ inner : {
+ x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x),
+ y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y)
+ },
+ outer : {
+ x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x),
+ y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y)
+ }
+ };
+ },
+ calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){
+ return Math.floor(Math.log(val) / Math.LN10);
+ },
+ calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){
+
+ //Set a minimum step of two - a point at the top of the graph, and a point at the base
+ var minSteps = 2,
+ maxSteps = Math.floor(drawingSize/(textSize * 1.5)),
+ skipFitting = (minSteps >= maxSteps);
+
+ var maxValue = max(valuesArray),
+ minValue = min(valuesArray);
+
+ // We need some degree of seperation here to calculate the scales if all the values are the same
+ // Adding/minusing 0.5 will give us a range of 1.
+ if (maxValue === minValue){
+ maxValue += 0.5;
+ // So we don't end up with a graph with a negative start value if we've said always start from zero
+ if (minValue >= 0.5 && !startFromZero){
+ minValue -= 0.5;
+ }
+ else{
+ // Make up a whole number above the values
+ maxValue += 0.5;
+ }
+ }
+
+ var valueRange = Math.abs(maxValue - minValue),
+ rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
+ graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
+ graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
+ graphRange = graphMax - graphMin,
+ stepValue = Math.pow(10, rangeOrderOfMagnitude),
+ numberOfSteps = Math.round(graphRange / stepValue);
+
+ //If we have more space on the graph we'll use it to give more definition to the data
+ while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
+ if(numberOfSteps > maxSteps){
+ stepValue *=2;
+ numberOfSteps = Math.round(graphRange/stepValue);
+ // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
+ if (numberOfSteps % 1 !== 0){
+ skipFitting = true;
+ }
+ }
+ //We can fit in double the amount of scale points on the scale
+ else{
+ //If user has declared ints only, and the step value isn't a decimal
+ if (integersOnly && rangeOrderOfMagnitude >= 0){
+ //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
+ if(stepValue/2 % 1 === 0){
+ stepValue /=2;
+ numberOfSteps = Math.round(graphRange/stepValue);
+ }
+ //If it would make it a float break out of the loop
+ else{
+ break;
+ }
+ }
+ //If the scale doesn't have to be an int, make the scale more granular anyway.
+ else{
+ stepValue /=2;
+ numberOfSteps = Math.round(graphRange/stepValue);
+ }
+
+ }
+ }
+
+ if (skipFitting){
+ numberOfSteps = minSteps;
+ stepValue = graphRange / numberOfSteps;
+ }
+
+ return {
+ steps : numberOfSteps,
+ stepValue : stepValue,
+ min : graphMin,
+ max : graphMin + (numberOfSteps * stepValue)
+ };
+
+ },
+ /* jshint ignore:start */
+ // Blows up jshint errors based on the new Function constructor
+ //Templating methods
+ //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
+ template = helpers.template = function(templateString, valuesObject){
+
+ // If templateString is function rather than string-template - call the function for valuesObject
+
+ if(templateString instanceof Function){
+ return templateString(valuesObject);
+ }
+
+ var cache = {};
+ function tmpl(str, data){
+ // Figure out if we're getting a template, or if we need to
+ // load the template - and be sure to cache the result.
+ var fn = !/\W/.test(str) ?
+ cache[str] = cache[str] :
+
+ // Generate a reusable function that will serve as a template
+ // generator (and which will be cached).
+ new Function("obj",
+ "var p=[],print=function(){p.push.apply(p,arguments);};" +
+
+ // Introduce the data as local variables using with(){}
+ "with(obj){p.push('" +
+
+ // Convert the template into pure JavaScript
+ str
+ .replace(/[\r\t\n]/g, " ")
+ .split("<%").join("\t")
+ .replace(/((^|%>)[^\t]*)'/g, "$1\r")
+ .replace(/\t=(.*?)%>/g, "',$1,'")
+ .split("\t").join("');")
+ .split("%>").join("p.push('")
+ .split("\r").join("\\'") +
+ "');}return p.join('');"
+ );
+
+ // Provide some basic currying to the user
+ return data ? fn( data ) : fn;
+ }
+ return tmpl(templateString,valuesObject);
+ },
+ /* jshint ignore:end */
+ generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){
+ var labelsArray = new Array(numberOfSteps);
+ if (labelTemplateString){
+ each(labelsArray,function(val,index){
+ labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))});
+ });
+ }
+ return labelsArray;
+ },
+ //--Animation methods
+ //Easing functions adapted from Robert Penner's easing equations
+ //http://www.robertpenner.com/easing/
+ easingEffects = helpers.easingEffects = {
+ linear: function (t) {
+ return t;
+ },
+ easeInQuad: function (t) {
+ return t * t;
+ },
+ easeOutQuad: function (t) {
+ return -1 * t * (t - 2);
+ },
+ easeInOutQuad: function (t) {
+ if ((t /= 1 / 2) < 1) return 1 / 2 * t * t;
+ return -1 / 2 * ((--t) * (t - 2) - 1);
+ },
+ easeInCubic: function (t) {
+ return t * t * t;
+ },
+ easeOutCubic: function (t) {
+ return 1 * ((t = t / 1 - 1) * t * t + 1);
+ },
+ easeInOutCubic: function (t) {
+ if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t;
+ return 1 / 2 * ((t -= 2) * t * t + 2);
+ },
+ easeInQuart: function (t) {
+ return t * t * t * t;
+ },
+ easeOutQuart: function (t) {
+ return -1 * ((t = t / 1 - 1) * t * t * t - 1);
+ },
+ easeInOutQuart: function (t) {
+ if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t;
+ return -1 / 2 * ((t -= 2) * t * t * t - 2);
+ },
+ easeInQuint: function (t) {
+ return 1 * (t /= 1) * t * t * t * t;
+ },
+ easeOutQuint: function (t) {
+ return 1 * ((t = t / 1 - 1) * t * t * t * t + 1);
+ },
+ easeInOutQuint: function (t) {
+ if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t;
+ return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
+ },
+ easeInSine: function (t) {
+ return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1;
+ },
+ easeOutSine: function (t) {
+ return 1 * Math.sin(t / 1 * (Math.PI / 2));
+ },
+ easeInOutSine: function (t) {
+ return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1);
+ },
+ easeInExpo: function (t) {
+ return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1));
+ },
+ easeOutExpo: function (t) {
+ return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1);
+ },
+ easeInOutExpo: function (t) {
+ if (t === 0) return 0;
+ if (t === 1) return 1;
+ if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1));
+ return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
+ },
+ easeInCirc: function (t) {
+ if (t >= 1) return t;
+ return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
+ },
+ easeOutCirc: function (t) {
+ return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
+ },
+ easeInOutCirc: function (t) {
+ if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1);
+ return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1);
+ },
+ easeInElastic: function (t) {
+ var s = 1.70158;
+ var p = 0;
+ var a = 1;
+ if (t === 0) return 0;
+ if ((t /= 1) == 1) return 1;
+ if (!p) p = 1 * 0.3;
+ if (a < Math.abs(1)) {
+ a = 1;
+ s = p / 4;
+ } else s = p / (2 * Math.PI) * Math.asin(1 / a);
+ return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
+ },
+ easeOutElastic: function (t) {
+ var s = 1.70158;
+ var p = 0;
+ var a = 1;
+ if (t === 0) return 0;
+ if ((t /= 1) == 1) return 1;
+ if (!p) p = 1 * 0.3;
+ if (a < Math.abs(1)) {
+ a = 1;
+ s = p / 4;
+ } else s = p / (2 * Math.PI) * Math.asin(1 / a);
+ return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
+ },
+ easeInOutElastic: function (t) {
+ var s = 1.70158;
+ var p = 0;
+ var a = 1;
+ if (t === 0) return 0;
+ if ((t /= 1 / 2) == 2) return 1;
+ if (!p) p = 1 * (0.3 * 1.5);
+ if (a < Math.abs(1)) {
+ a = 1;
+ s = p / 4;
+ } else s = p / (2 * Math.PI) * Math.asin(1 / a);
+ if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
+ return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1;
+ },
+ easeInBack: function (t) {
+ var s = 1.70158;
+ return 1 * (t /= 1) * t * ((s + 1) * t - s);
+ },
+ easeOutBack: function (t) {
+ var s = 1.70158;
+ return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1);
+ },
+ easeInOutBack: function (t) {
+ var s = 1.70158;
+ if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s));
+ return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);
+ },
+ easeInBounce: function (t) {
+ return 1 - easingEffects.easeOutBounce(1 - t);
+ },
+ easeOutBounce: function (t) {
+ if ((t /= 1) < (1 / 2.75)) {
+ return 1 * (7.5625 * t * t);
+ } else if (t < (2 / 2.75)) {
+ return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
+ } else if (t < (2.5 / 2.75)) {
+ return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
+ } else {
+ return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
+ }
+ },
+ easeInOutBounce: function (t) {
+ if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5;
+ return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5;
+ }
+ },
+ //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
+ requestAnimFrame = helpers.requestAnimFrame = (function(){
+ return window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(callback) {
+ return window.setTimeout(callback, 1000 / 60);
+ };
+ })(),
+ cancelAnimFrame = helpers.cancelAnimFrame = (function(){
+ return window.cancelAnimationFrame ||
+ window.webkitCancelAnimationFrame ||
+ window.mozCancelAnimationFrame ||
+ window.oCancelAnimationFrame ||
+ window.msCancelAnimationFrame ||
+ function(callback) {
+ return window.clearTimeout(callback, 1000 / 60);
+ };
+ })(),
+ animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){
+
+ var currentStep = 0,
+ easingFunction = easingEffects[easingString] || easingEffects.linear;
+
+ var animationFrame = function(){
+ currentStep++;
+ var stepDecimal = currentStep/totalSteps;
+ var easeDecimal = easingFunction(stepDecimal);
+
+ callback.call(chartInstance,easeDecimal,stepDecimal, currentStep);
+ onProgress.call(chartInstance,easeDecimal,stepDecimal);
+ if (currentStep < totalSteps){
+ chartInstance.animationFrame = requestAnimFrame(animationFrame);
+ } else{
+ onComplete.apply(chartInstance);
+ }
+ };
+ requestAnimFrame(animationFrame);
+ },
+ //-- DOM methods
+ getRelativePosition = helpers.getRelativePosition = function(evt){
+ var mouseX, mouseY;
+ var e = evt.originalEvent || evt,
+ canvas = evt.currentTarget || evt.srcElement,
+ boundingRect = canvas.getBoundingClientRect();
+
+ if (e.touches){
+ mouseX = e.touches[0].clientX - boundingRect.left;
+ mouseY = e.touches[0].clientY - boundingRect.top;
+
+ }
+ else{
+ mouseX = e.clientX - boundingRect.left;
+ mouseY = e.clientY - boundingRect.top;
+ }
+
+ return {
+ x : mouseX,
+ y : mouseY
+ };
+
+ },
+ addEvent = helpers.addEvent = function(node,eventType,method){
+ if (node.addEventListener){
+ node.addEventListener(eventType,method);
+ } else if (node.attachEvent){
+ node.attachEvent("on"+eventType, method);
+ } else {
+ node["on"+eventType] = method;
+ }
+ },
+ removeEvent = helpers.removeEvent = function(node, eventType, handler){
+ if (node.removeEventListener){
+ node.removeEventListener(eventType, handler, false);
+ } else if (node.detachEvent){
+ node.detachEvent("on"+eventType,handler);
+ } else{
+ node["on" + eventType] = noop;
+ }
+ },
+ bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){
+ // Create the events object if it's not already present
+ if (!chartInstance.events) chartInstance.events = {};
+
+ each(arrayOfEvents,function(eventName){
+ chartInstance.events[eventName] = function(){
+ handler.apply(chartInstance, arguments);
+ };
+ addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]);
+ });
+ },
+ unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) {
+ each(arrayOfEvents, function(handler,eventName){
+ removeEvent(chartInstance.chart.canvas, eventName, handler);
+ });
+ },
+ getMaximumWidth = helpers.getMaximumWidth = function(domNode){
+ var container = domNode.parentNode;
+ // TODO = check cross browser stuff with this.
+ return container.clientWidth;
+ },
+ getMaximumHeight = helpers.getMaximumHeight = function(domNode){
+ var container = domNode.parentNode;
+ // TODO = check cross browser stuff with this.
+ return container.clientHeight;
+ },
+ getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support
+ retinaScale = helpers.retinaScale = function(chart){
+ var ctx = chart.ctx,
+ width = chart.canvas.width,
+ height = chart.canvas.height;
+
+ if (window.devicePixelRatio) {
+ ctx.canvas.style.width = width + "px";
+ ctx.canvas.style.height = height + "px";
+ ctx.canvas.height = height * window.devicePixelRatio;
+ ctx.canvas.width = width * window.devicePixelRatio;
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+ }
+ },
+ //-- Canvas methods
+ clear = helpers.clear = function(chart){
+ chart.ctx.clearRect(0,0,chart.width,chart.height);
+ },
+ fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){
+ return fontStyle + " " + pixelSize+"px " + fontFamily;
+ },
+ longestText = helpers.longestText = function(ctx,font,arrayOfStrings){
+ ctx.font = font;
+ var longest = 0;
+ each(arrayOfStrings,function(string){
+ var textWidth = ctx.measureText(string).width;
+ longest = (textWidth > longest) ? textWidth : longest;
+ });
+ return longest;
+ },
+ drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+ };
+
+
+ //Store a reference to each instance - allowing us to globally resize chart instances on window resize.
+ //Destroy method on the chart will remove the instance of the chart from this reference.
+ Chart.instances = {};
+
+ Chart.Type = function(data,options,chart){
+ this.options = options;
+ this.chart = chart;
+ this.id = uid();
+ //Add the chart instance to the global namespace
+ Chart.instances[this.id] = this;
+
+ // Initialize is always called when a chart type is created
+ // By default it is a no op, but it should be extended
+ if (options.responsive){
+ this.resize();
+ }
+ this.initialize.call(this,data);
+ };
+
+ //Core methods that'll be a part of every chart type
+ extend(Chart.Type.prototype,{
+ initialize : function(){return this;},
+ clear : function(){
+ clear(this.chart);
+ return this;
+ },
+ stop : function(){
+ // Stops any current animation loop occuring
+ cancelAnimFrame(this.animationFrame);
+ return this;
+ },
+ resize : function(callback){
+ this.stop();
+ var canvas = this.chart.canvas,
+ newWidth = getMaximumWidth(this.chart.canvas),
+ newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
+
+ canvas.width = this.chart.width = newWidth;
+ canvas.height = this.chart.height = newHeight;
+
+ retinaScale(this.chart);
+
+ if (typeof callback === "function"){
+ callback.apply(this, Array.prototype.slice.call(arguments, 1));
+ }
+ return this;
+ },
+ reflow : noop,
+ render : function(reflow){
+ if (reflow){
+ this.reflow();
+ }
+ if (this.options.animation && !reflow){
+ helpers.animationLoop(
+ this.draw,
+ this.options.animationSteps,
+ this.options.animationEasing,
+ this.options.onAnimationProgress,
+ this.options.onAnimationComplete,
+ this
+ );
+ }
+ else{
+ this.draw();
+ this.options.onAnimationComplete.call(this);
+ }
+ return this;
+ },
+ generateLegend : function(){
+ return template(this.options.legendTemplate,this);
+ },
+ destroy : function(){
+ this.clear();
+ unbindEvents(this, this.events);
+ var canvas = this.chart.canvas;
+
+ // Reset canvas height/width attributes starts a fresh with the canvas context
+ canvas.width = this.chart.width;
+ canvas.height = this.chart.height;
+
+ // < IE9 doesn't support removeProperty
+ if (canvas.style.removeProperty) {
+ canvas.style.removeProperty('width');
+ canvas.style.removeProperty('height');
+ } else {
+ canvas.style.removeAttribute('width');
+ canvas.style.removeAttribute('height');
+ }
+
+ delete Chart.instances[this.id];
+ },
+ showTooltip : function(ChartElements, forceRedraw){
+ // Only redraw the chart if we've actually changed what we're hovering on.
+ if (typeof this.activeElements === 'undefined') this.activeElements = [];
+
+ var isChanged = (function(Elements){
+ var changed = false;
+
+ if (Elements.length !== this.activeElements.length){
+ changed = true;
+ return changed;
+ }
+
+ each(Elements, function(element, index){
+ if (element !== this.activeElements[index]){
+ changed = true;
+ }
+ }, this);
+ return changed;
+ }).call(this, ChartElements);
+
+ if (!isChanged && !forceRedraw){
+ return;
+ }
+ else{
+ this.activeElements = ChartElements;
+ }
+ this.draw();
+ if(this.options.customTooltips){
+ this.options.customTooltips(false);
+ }
+ if (ChartElements.length > 0){
+ // If we have multiple datasets, show a MultiTooltip for all of the data points at that index
+ if (this.datasets && this.datasets.length > 1) {
+ var dataArray,
+ dataIndex;
+
+ for (var i = this.datasets.length - 1; i >= 0; i--) {
+ dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
+ dataIndex = indexOf(dataArray, ChartElements[0]);
+ if (dataIndex !== -1){
+ break;
+ }
+ }
+ var tooltipLabels = [],
+ tooltipColors = [],
+ medianPosition = (function(index) {
+
+ // Get all the points at that particular index
+ var Elements = [],
+ dataCollection,
+ xPositions = [],
+ yPositions = [],
+ xMax,
+ yMax,
+ xMin,
+ yMin;
+ helpers.each(this.datasets, function(dataset){
+ dataCollection = dataset.points || dataset.bars || dataset.segments;
+ if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){
+ Elements.push(dataCollection[dataIndex]);
+ }
+ });
+
+ helpers.each(Elements, function(element) {
+ xPositions.push(element.x);
+ yPositions.push(element.y);
+
+
+ //Include any colour information about the element
+ tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
+ tooltipColors.push({
+ fill: element._saved.fillColor || element.fillColor,
+ stroke: element._saved.strokeColor || element.strokeColor
+ });
+
+ }, this);
+
+ yMin = min(yPositions);
+ yMax = max(yPositions);
+
+ xMin = min(xPositions);
+ xMax = max(xPositions);
+
+ return {
+ x: (xMin > this.chart.width/2) ? xMin : xMax,
+ y: (yMin + yMax)/2
+ };
+ }).call(this, dataIndex);
+
+ new Chart.MultiTooltip({
+ x: medianPosition.x,
+ y: medianPosition.y,
+ xPadding: this.options.tooltipXPadding,
+ yPadding: this.options.tooltipYPadding,
+ xOffset: this.options.tooltipXOffset,
+ fillColor: this.options.tooltipFillColor,
+ textColor: this.options.tooltipFontColor,
+ fontFamily: this.options.tooltipFontFamily,
+ fontStyle: this.options.tooltipFontStyle,
+ fontSize: this.options.tooltipFontSize,
+ titleTextColor: this.options.tooltipTitleFontColor,
+ titleFontFamily: this.options.tooltipTitleFontFamily,
+ titleFontStyle: this.options.tooltipTitleFontStyle,
+ titleFontSize: this.options.tooltipTitleFontSize,
+ cornerRadius: this.options.tooltipCornerRadius,
+ labels: tooltipLabels,
+ legendColors: tooltipColors,
+ legendColorBackground : this.options.multiTooltipKeyBackground,
+ title: ChartElements[0].label,
+ chart: this.chart,
+ ctx: this.chart.ctx,
+ custom: this.options.customTooltips
+ }).draw();
+
+ } else {
+ each(ChartElements, function(Element) {
+ var tooltipPosition = Element.tooltipPosition();
+ new Chart.Tooltip({
+ x: Math.round(tooltipPosition.x),
+ y: Math.round(tooltipPosition.y),
+ xPadding: this.options.tooltipXPadding,
+ yPadding: this.options.tooltipYPadding,
+ fillColor: this.options.tooltipFillColor,
+ textColor: this.options.tooltipFontColor,
+ fontFamily: this.options.tooltipFontFamily,
+ fontStyle: this.options.tooltipFontStyle,
+ fontSize: this.options.tooltipFontSize,
+ caretHeight: this.options.tooltipCaretSize,
+ cornerRadius: this.options.tooltipCornerRadius,
+ text: template(this.options.tooltipTemplate, Element),
+ chart: this.chart,
+ custom: this.options.customTooltips
+ }).draw();
+ }, this);
+ }
+ }
+ return this;
+ },
+ toBase64Image : function(){
+ return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
+ }
+ });
+
+ Chart.Type.extend = function(extensions){
+
+ var parent = this;
+
+ var ChartType = function(){
+ return parent.apply(this,arguments);
+ };
+
+ //Copy the prototype object of the this class
+ ChartType.prototype = clone(parent.prototype);
+ //Now overwrite some of the properties in the base class with the new extensions
+ extend(ChartType.prototype, extensions);
+
+ ChartType.extend = Chart.Type.extend;
+
+ if (extensions.name || parent.prototype.name){
+
+ var chartName = extensions.name || parent.prototype.name;
+ //Assign any potential default values of the new chart type
+
+ //If none are defined, we'll use a clone of the chart type this is being extended from.
+ //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
+ //doesn't define some defaults of their own.
+
+ var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {};
+
+ Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults);
+
+ Chart.types[chartName] = ChartType;
+
+ //Register this new chart type in the Chart prototype
+ Chart.prototype[chartName] = function(data,options){
+ var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {});
+ return new ChartType(data,config,this);
+ };
+ } else{
+ warn("Name not provided for this chart, so it hasn't been registered");
+ }
+ return parent;
+ };
+
+ Chart.Element = function(configuration){
+ extend(this,configuration);
+ this.initialize.apply(this,arguments);
+ this.save();
+ };
+ extend(Chart.Element.prototype,{
+ initialize : function(){},
+ restore : function(props){
+ if (!props){
+ extend(this,this._saved);
+ } else {
+ each(props,function(key){
+ this[key] = this._saved[key];
+ },this);
+ }
+ return this;
+ },
+ save : function(){
+ this._saved = clone(this);
+ delete this._saved._saved;
+ return this;
+ },
+ update : function(newProps){
+ each(newProps,function(value,key){
+ this._saved[key] = this[key];
+ this[key] = value;
+ },this);
+ return this;
+ },
+ transition : function(props,ease){
+ each(props,function(value,key){
+ this[key] = ((value - this._saved[key]) * ease) + this._saved[key];
+ },this);
+ return this;
+ },
+ tooltipPosition : function(){
+ return {
+ x : this.x,
+ y : this.y
+ };
+ },
+ hasValue: function(){
+ return isNumber(this.value);
+ }
+ });
+
+ Chart.Element.extend = inherits;
+
+
+ Chart.Point = Chart.Element.extend({
+ display: true,
+ inRange: function(chartX,chartY){
+ var hitDetectionRange = this.hitDetectionRadius + this.radius;
+ return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2));
+ },
+ draw : function(){
+ if (this.display){
+ var ctx = this.ctx;
+ ctx.beginPath();
+
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
+ ctx.closePath();
+
+ ctx.strokeStyle = this.strokeColor;
+ ctx.lineWidth = this.strokeWidth;
+
+ ctx.fillStyle = this.fillColor;
+
+ ctx.fill();
+ ctx.stroke();
+ }
+
+
+ //Quick debug for bezier curve splining
+ //Highlights control points and the line between them.
+ //Handy for dev - stripped in the min version.
+
+ // ctx.save();
+ // ctx.fillStyle = "black";
+ // ctx.strokeStyle = "black"
+ // ctx.beginPath();
+ // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2);
+ // ctx.fill();
+
+ // ctx.beginPath();
+ // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2);
+ // ctx.fill();
+
+ // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y);
+ // ctx.lineTo(this.x, this.y);
+ // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y);
+ // ctx.stroke();
+
+ // ctx.restore();
+
+
+
+ }
+ });
+
+ Chart.Arc = Chart.Element.extend({
+ inRange : function(chartX,chartY){
+
+ var pointRelativePosition = helpers.getAngleFromPoint(this, {
+ x: chartX,
+ y: chartY
+ });
+
+ //Check if within the range of the open/close angle
+ var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle),
+ withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius);
+
+ return (betweenAngles && withinRadius);
+ //Ensure within the outside of the arc centre, but inside arc outer
+ },
+ tooltipPosition : function(){
+ var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2),
+ rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius;
+ return {
+ x : this.x + (Math.cos(centreAngle) * rangeFromCentre),
+ y : this.y + (Math.sin(centreAngle) * rangeFromCentre)
+ };
+ },
+ draw : function(animationPercent){
+
+ var easingDecimal = animationPercent || 1;
+
+ var ctx = this.ctx;
+
+ ctx.beginPath();
+
+ ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle);
+
+ ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true);
+
+ ctx.closePath();
+ ctx.strokeStyle = this.strokeColor;
+ ctx.lineWidth = this.strokeWidth;
+
+ ctx.fillStyle = this.fillColor;
+
+ ctx.fill();
+ ctx.lineJoin = 'bevel';
+
+ if (this.showStroke){
+ ctx.stroke();
+ }
+ }
+ });
+
+ Chart.Rectangle = Chart.Element.extend({
+ draw : function(){
+ var ctx = this.ctx,
+ halfWidth = this.width/2,
+ leftX = this.x - halfWidth,
+ rightX = this.x + halfWidth,
+ top = this.base - (this.base - this.y),
+ halfStroke = this.strokeWidth / 2;
+
+ // Canvas doesn't allow us to stroke inside the width so we can
+ // adjust the sizes to fit if we're setting a stroke on the line
+ if (this.showStroke){
+ leftX += halfStroke;
+ rightX -= halfStroke;
+ top += halfStroke;
+ }
+
+ ctx.beginPath();
+
+ ctx.fillStyle = this.fillColor;
+ ctx.strokeStyle = this.strokeColor;
+ ctx.lineWidth = this.strokeWidth;
+
+ // It'd be nice to keep this class totally generic to any rectangle
+ // and simply specify which border to miss out.
+ ctx.moveTo(leftX, this.base);
+ ctx.lineTo(leftX, top);
+ ctx.lineTo(rightX, top);
+ ctx.lineTo(rightX, this.base);
+ ctx.fill();
+ if (this.showStroke){
+ ctx.stroke();
+ }
+ },
+ height : function(){
+ return this.base - this.y;
+ },
+ inRange : function(chartX,chartY){
+ return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base);
+ }
+ });
+
+ Chart.Tooltip = Chart.Element.extend({
+ draw : function(){
+
+ var ctx = this.chart.ctx;
+
+ ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+
+ this.xAlign = "center";
+ this.yAlign = "above";
+
+ //Distance between the actual element.y position and the start of the tooltip caret
+ var caretPadding = this.caretPadding = 2;
+
+ var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding,
+ tooltipRectHeight = this.fontSize + 2*this.yPadding,
+ tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding;
+
+ if (this.x + tooltipWidth/2 >this.chart.width){
+ this.xAlign = "left";
+ } else if (this.x - tooltipWidth/2 < 0){
+ this.xAlign = "right";
+ }
+
+ if (this.y - tooltipHeight < 0){
+ this.yAlign = "below";
+ }
+
+
+ var tooltipX = this.x - tooltipWidth/2,
+ tooltipY = this.y - tooltipHeight;
+
+ ctx.fillStyle = this.fillColor;
+
+ // Custom Tooltips
+ if(this.custom){
+ this.custom(this);
+ }
+ else{
+ switch(this.yAlign)
+ {
+ case "above":
+ //Draw a caret above the x/y
+ ctx.beginPath();
+ ctx.moveTo(this.x,this.y - caretPadding);
+ ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight));
+ ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight));
+ ctx.closePath();
+ ctx.fill();
+ break;
+ case "below":
+ tooltipY = this.y + caretPadding + this.caretHeight;
+ //Draw a caret below the x/y
+ ctx.beginPath();
+ ctx.moveTo(this.x, this.y + caretPadding);
+ ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight);
+ ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight);
+ ctx.closePath();
+ ctx.fill();
+ break;
+ }
+
+ switch(this.xAlign)
+ {
+ case "left":
+ tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight);
+ break;
+ case "right":
+ tooltipX = this.x - (this.cornerRadius + this.caretHeight);
+ break;
+ }
+
+ drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius);
+
+ ctx.fill();
+
+ ctx.fillStyle = this.textColor;
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2);
+ }
+ }
+ });
+
+ Chart.MultiTooltip = Chart.Element.extend({
+ initialize : function(){
+ this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+
+ this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily);
+
+ this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5;
+
+ this.ctx.font = this.titleFont;
+
+ var titleWidth = this.ctx.measureText(this.title).width,
+ //Label has a legend square as well so account for this.
+ labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3,
+ longestTextWidth = max([labelWidth,titleWidth]);
+
+ this.width = longestTextWidth + (this.xPadding*2);
+
+
+ var halfHeight = this.height/2;
+
+ //Check to ensure the height will fit on the canvas
+ if (this.y - halfHeight < 0 ){
+ this.y = halfHeight;
+ } else if (this.y + halfHeight > this.chart.height){
+ this.y = this.chart.height - halfHeight;
+ }
+
+ //Decide whether to align left or right based on position on canvas
+ if (this.x > this.chart.width/2){
+ this.x -= this.xOffset + this.width;
+ } else {
+ this.x += this.xOffset;
+ }
+
+
+ },
+ getLineHeight : function(index){
+ var baseLineHeight = this.y - (this.height/2) + this.yPadding,
+ afterTitleIndex = index-1;
+
+ //If the index is zero, we're getting the title
+ if (index === 0){
+ return baseLineHeight + this.titleFontSize/2;
+ } else{
+ return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5;
+ }
+
+ },
+ draw : function(){
+ // Custom Tooltips
+ if(this.custom){
+ this.custom(this);
+ }
+ else{
+ drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius);
+ var ctx = this.ctx;
+ ctx.fillStyle = this.fillColor;
+ ctx.fill();
+ ctx.closePath();
+
+ ctx.textAlign = "left";
+ ctx.textBaseline = "middle";
+ ctx.fillStyle = this.titleTextColor;
+ ctx.font = this.titleFont;
+
+ ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0));
+
+ ctx.font = this.font;
+ helpers.each(this.labels,function(label,index){
+ ctx.fillStyle = this.textColor;
+ ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1));
+
+ //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
+ //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+ //Instead we'll make a white filled block to put the legendColour palette over.
+
+ ctx.fillStyle = this.legendColorBackground;
+ ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+
+ ctx.fillStyle = this.legendColors[index].fill;
+ ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+
+
+ },this);
+ }
+ }
+ });
+
+ Chart.Scale = Chart.Element.extend({
+ initialize : function(){
+ this.fit();
+ },
+ buildYLabels : function(){
+ this.yLabels = [];
+
+ var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
+
+ for (var i=0; i<=this.steps; i++){
+ this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
+ }
+ this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0;
+ },
+ addXLabel : function(label){
+ this.xLabels.push(label);
+ this.valuesCount++;
+ this.fit();
+ },
+ removeXLabel : function(){
+ this.xLabels.shift();
+ this.valuesCount--;
+ this.fit();
+ },
+ // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use
+ fit: function(){
+ // First we need the width of the yLabels, assuming the xLabels aren't rotated
+
+ // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation
+ this.startPoint = (this.display) ? this.fontSize : 0;
+ this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels
+
+ // Apply padding settings to the start and end point.
+ this.startPoint += this.padding;
+ this.endPoint -= this.padding;
+
+ // Cache the starting height, so can determine if we need to recalculate the scale yAxis
+ var cachedHeight = this.endPoint - this.startPoint,
+ cachedYLabelWidth;
+
+ // Build the current yLabels so we have an idea of what size they'll be to start
+ /*
+ * This sets what is returned from calculateScaleRange as static properties of this class:
+ *
+ this.steps;
+ this.stepValue;
+ this.min;
+ this.max;
+ *
+ */
+ this.calculateYRange(cachedHeight);
+
+ // With these properties set we can now build the array of yLabels
+ // and also the width of the largest yLabel
+ this.buildYLabels();
+
+ this.calculateXLabelRotation();
+
+ while((cachedHeight > this.endPoint - this.startPoint)){
+ cachedHeight = this.endPoint - this.startPoint;
+ cachedYLabelWidth = this.yLabelWidth;
+
+ this.calculateYRange(cachedHeight);
+ this.buildYLabels();
+
+ // Only go through the xLabel loop again if the yLabel width has changed
+ if (cachedYLabelWidth < this.yLabelWidth){
+ this.calculateXLabelRotation();
+ }
+ }
+
+ },
+ calculateXLabelRotation : function(){
+ //Get the width of each grid by calculating the difference
+ //between x offsets between 0 and 1.
+
+ this.ctx.font = this.font;
+
+ var firstWidth = this.ctx.measureText(this.xLabels[0]).width,
+ lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width,
+ firstRotated,
+ lastRotated;
+
+
+ this.xScalePaddingRight = lastWidth/2 + 3;
+ this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10;
+
+ this.xLabelRotation = 0;
+ if (this.display){
+ var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels),
+ cosRotation,
+ firstRotatedWidth;
+ this.xLabelWidth = originalLabelWidth;
+ //Allow 3 pixels x2 padding either side for label readability
+ var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6;
+
+ //Max label rotate should be 90 - also act as a loop counter
+ while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){
+ cosRotation = Math.cos(toRadians(this.xLabelRotation));
+
+ firstRotated = cosRotation * firstWidth;
+ lastRotated = cosRotation * lastWidth;
+
+ // We're right aligning the text now.
+ if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){
+ this.xScalePaddingLeft = firstRotated + this.fontSize / 2;
+ }
+ this.xScalePaddingRight = this.fontSize/2;
+
+
+ this.xLabelRotation++;
+ this.xLabelWidth = cosRotation * originalLabelWidth;
+
+ }
+ if (this.xLabelRotation > 0){
+ this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3;
+ }
+ }
+ else{
+ this.xLabelWidth = 0;
+ this.xScalePaddingRight = this.padding;
+ this.xScalePaddingLeft = this.padding;
+ }
+
+ },
+ // Needs to be overidden in each Chart type
+ // Otherwise we need to pass all the data into the scale class
+ calculateYRange: noop,
+ drawingArea: function(){
+ return this.startPoint - this.endPoint;
+ },
+ calculateY : function(value){
+ var scalingFactor = this.drawingArea() / (this.min - this.max);
+ return this.endPoint - (scalingFactor * (value - this.min));
+ },
+ calculateX : function(index){
+ var isRotated = (this.xLabelRotation > 0),
+ // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
+ innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight),
+ valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1),
+ valueOffset = (valueWidth * index) + this.xScalePaddingLeft;
+
+ if (this.offsetGridLines){
+ valueOffset += (valueWidth/2);
+ }
+
+ return Math.round(valueOffset);
+ },
+ update : function(newProps){
+ helpers.extend(this, newProps);
+ this.fit();
+ },
+ draw : function(){
+ var ctx = this.ctx,
+ yLabelGap = (this.endPoint - this.startPoint) / this.steps,
+ xStart = Math.round(this.xScalePaddingLeft);
+ if (this.display){
+ ctx.fillStyle = this.textColor;
+ ctx.font = this.font;
+ each(this.yLabels,function(labelString,index){
+ var yLabelCenter = this.endPoint - (yLabelGap * index),
+ linePositionY = Math.round(yLabelCenter),
+ drawHorizontalLine = this.showHorizontalLines;
+
+ ctx.textAlign = "right";
+ ctx.textBaseline = "middle";
+ if (this.showLabels){
+ ctx.fillText(labelString,xStart - 10,yLabelCenter);
+ }
+
+ // This is X axis, so draw it
+ if (index === 0 && !drawHorizontalLine){
+ drawHorizontalLine = true;
+ }
+
+ if (drawHorizontalLine){
+ ctx.beginPath();
+ }
+
+ if (index > 0){
+ // This is a grid line in the centre, so drop that
+ ctx.lineWidth = this.gridLineWidth;
+ ctx.strokeStyle = this.gridLineColor;
+ } else {
+ // This is the first line on the scale
+ ctx.lineWidth = this.lineWidth;
+ ctx.strokeStyle = this.lineColor;
+ }
+
+ linePositionY += helpers.aliasPixel(ctx.lineWidth);
+
+ if(drawHorizontalLine){
+ ctx.moveTo(xStart, linePositionY);
+ ctx.lineTo(this.width, linePositionY);
+ ctx.stroke();
+ ctx.closePath();
+ }
+
+ ctx.lineWidth = this.lineWidth;
+ ctx.strokeStyle = this.lineColor;
+ ctx.beginPath();
+ ctx.moveTo(xStart - 5, linePositionY);
+ ctx.lineTo(xStart, linePositionY);
+ ctx.stroke();
+ ctx.closePath();
+
+ },this);
+
+ each(this.xLabels,function(label,index){
+ var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
+ // Check to see if line/bar here and decide where to place the line
+ linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
+ isRotated = (this.xLabelRotation > 0),
+ drawVerticalLine = this.showVerticalLines;
+
+ // This is Y axis, so draw it
+ if (index === 0 && !drawVerticalLine){
+ drawVerticalLine = true;
+ }
+
+ if (drawVerticalLine){
+ ctx.beginPath();
+ }
+
+ if (index > 0){
+ // This is a grid line in the centre, so drop that
+ ctx.lineWidth = this.gridLineWidth;
+ ctx.strokeStyle = this.gridLineColor;
+ } else {
+ // This is the first line on the scale
+ ctx.lineWidth = this.lineWidth;
+ ctx.strokeStyle = this.lineColor;
+ }
+
+ if (drawVerticalLine){
+ ctx.moveTo(linePos,this.endPoint);
+ ctx.lineTo(linePos,this.startPoint - 3);
+ ctx.stroke();
+ ctx.closePath();
+ }
+
+
+ ctx.lineWidth = this.lineWidth;
+ ctx.strokeStyle = this.lineColor;
+
+
+ // Small lines at the bottom of the base grid line
+ ctx.beginPath();
+ ctx.moveTo(linePos,this.endPoint);
+ ctx.lineTo(linePos,this.endPoint + 5);
+ ctx.stroke();
+ ctx.closePath();
+
+ ctx.save();
+ ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8);
+ ctx.rotate(toRadians(this.xLabelRotation)*-1);
+ ctx.font = this.font;
+ ctx.textAlign = (isRotated) ? "right" : "center";
+ ctx.textBaseline = (isRotated) ? "middle" : "top";
+ ctx.fillText(label, 0, 0);
+ ctx.restore();
+ },this);
+
+ }
+ }
+
+ });
+
+ Chart.RadialScale = Chart.Element.extend({
+ initialize: function(){
+ this.size = min([this.height, this.width]);
+ this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
+ },
+ calculateCenterOffset: function(value){
+ // Take into account half font size + the yPadding of the top value
+ var scalingFactor = this.drawingArea / (this.max - this.min);
+
+ return (value - this.min) * scalingFactor;
+ },
+ update : function(){
+ if (!this.lineArc){
+ this.setScaleSize();
+ } else {
+ this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
+ }
+ this.buildYLabels();
+ },
+ buildYLabels: function(){
+ this.yLabels = [];
+
+ var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
+
+ for (var i=0; i<=this.steps; i++){
+ this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
+ }
+ },
+ getCircumference : function(){
+ return ((Math.PI*2) / this.valuesCount);
+ },
+ setScaleSize: function(){
+ /*
+ * Right, this is really confusing and there is a lot of maths going on here
+ * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
+ *
+ * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
+ *
+ * Solution:
+ *
+ * We assume the radius of the polygon is half the size of the canvas at first
+ * at each index we check if the text overlaps.
+ *
+ * Where it does, we store that angle and that index.
+ *
+ * After finding the largest index and angle we calculate how much we need to remove
+ * from the shape radius to move the point inwards by that x.
+ *
+ * We average the left and right distances to get the maximum shape radius that can fit in the box
+ * along with labels.
+ *
+ * Once we have that, we can find the centre point for the chart, by taking the x text protrusion
+ * on each side, removing that from the size, halving it and adding the left x protrusion width.
+ *
+ * This will mean we have a shape fitted to the canvas, as large as it can be with the labels
+ * and position it in the most space efficient manner
+ *
+ * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
+ */
+
+
+ // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
+ // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
+ var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]),
+ pointPosition,
+ i,
+ textWidth,
+ halfTextWidth,
+ furthestRight = this.width,
+ furthestRightIndex,
+ furthestRightAngle,
+ furthestLeft = 0,
+ furthestLeftIndex,
+ furthestLeftAngle,
+ xProtrusionLeft,
+ xProtrusionRight,
+ radiusReductionRight,
+ radiusReductionLeft,
+ maxWidthRadius;
+ this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
+ for (i=0;i<this.valuesCount;i++){
+ // 5px to space the text slightly out - similar to what we do in the draw function.
+ pointPosition = this.getPointPosition(i, largestPossibleRadius);
+ textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5;
+ if (i === 0 || i === this.valuesCount/2){
+ // If we're at index zero, or exactly the middle, we're at exactly the top/bottom
+ // of the radar chart, so text will be aligned centrally, so we'll half it and compare
+ // w/left and right text sizes
+ halfTextWidth = textWidth/2;
+ if (pointPosition.x + halfTextWidth > furthestRight) {
+ furthestRight = pointPosition.x + halfTextWidth;
+ furthestRightIndex = i;
+ }
+ if (pointPosition.x - halfTextWidth < furthestLeft) {
+ furthestLeft = pointPosition.x - halfTextWidth;
+ furthestLeftIndex = i;
+ }
+ }
+ else if (i < this.valuesCount/2) {
+ // Less than half the values means we'll left align the text
+ if (pointPosition.x + textWidth > furthestRight) {
+ furthestRight = pointPosition.x + textWidth;
+ furthestRightIndex = i;
+ }
+ }
+ else if (i > this.valuesCount/2){
+ // More than half the values means we'll right align the text
+ if (pointPosition.x - textWidth < furthestLeft) {
+ furthestLeft = pointPosition.x - textWidth;
+ furthestLeftIndex = i;
+ }
+ }
+ }
+
+ xProtrusionLeft = furthestLeft;
+
+ xProtrusionRight = Math.ceil(furthestRight - this.width);
+
+ furthestRightAngle = this.getIndexAngle(furthestRightIndex);
+
+ furthestLeftAngle = this.getIndexAngle(furthestLeftIndex);
+
+ radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2);
+
+ radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2);
+
+ // Ensure we actually need to reduce the size of the chart
+ radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0;
+ radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0;
+
+ this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2;
+
+ //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2])
+ this.setCenterPoint(radiusReductionLeft, radiusReductionRight);
+
+ },
+ setCenterPoint: function(leftMovement, rightMovement){
+
+ var maxRight = this.width - rightMovement - this.drawingArea,
+ maxLeft = leftMovement + this.drawingArea;
+
+ this.xCenter = (maxLeft + maxRight)/2;
+ // Always vertically in the centre as the text height doesn't change
+ this.yCenter = (this.height/2);
+ },
+
+ getIndexAngle : function(index){
+ var angleMultiplier = (Math.PI * 2) / this.valuesCount;
+ // Start from the top instead of right, so remove a quarter of the circle
+
+ return index * angleMultiplier - (Math.PI/2);
+ },
+ getPointPosition : function(index, distanceFromCenter){
+ var thisAngle = this.getIndexAngle(index);
+ return {
+ x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter,
+ y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter
+ };
+ },
+ draw: function(){
+ if (this.display){
+ var ctx = this.ctx;
+ each(this.yLabels, function(label, index){
+ // Don't draw a centre value
+ if (index > 0){
+ var yCenterOffset = index * (this.drawingArea/this.steps),
+ yHeight = this.yCenter - yCenterOffset,
+ pointPosition;
+
+ // Draw circular lines around the scale
+ if (this.lineWidth > 0){
+ ctx.strokeStyle = this.lineColor;
+ ctx.lineWidth = this.lineWidth;
+
+ if(this.lineArc){
+ ctx.beginPath();
+ ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2);
+ ctx.closePath();
+ ctx.stroke();
+ } else{
+ ctx.beginPath();
+ for (var i=0;i<this.valuesCount;i++)
+ {
+ pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue)));
+ if (i === 0){
+ ctx.moveTo(pointPosition.x, pointPosition.y);
+ } else {
+ ctx.lineTo(pointPosition.x, pointPosition.y);
+ }
+ }
+ ctx.closePath();
+ ctx.stroke();
+ }
+ }
+ if(this.showLabels){
+ ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+ if (this.showLabelBackdrop){
+ var labelWidth = ctx.measureText(label).width;
+ ctx.fillStyle = this.backdropColor;
+ ctx.fillRect(
+ this.xCenter - labelWidth/2 - this.backdropPaddingX,
+ yHeight - this.fontSize/2 - this.backdropPaddingY,
+ labelWidth + this.backdropPaddingX*2,
+ this.fontSize + this.backdropPaddingY*2
+ );
+ }
+ ctx.textAlign = 'center';
+ ctx.textBaseline = "middle";
+ ctx.fillStyle = this.fontColor;
+ ctx.fillText(label, this.xCenter, yHeight);
+ }
+ }
+ }, this);
+
+ if (!this.lineArc){
+ ctx.lineWidth = this.angleLineWidth;
+ ctx.strokeStyle = this.angleLineColor;
+ for (var i = this.valuesCount - 1; i >= 0; i--) {
+ if (this.angleLineWidth > 0){
+ var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max));
+ ctx.beginPath();
+ ctx.moveTo(this.xCenter, this.yCenter);
+ ctx.lineTo(outerPosition.x, outerPosition.y);
+ ctx.stroke();
+ ctx.closePath();
+ }
+ // Extra 3px out for some label spacing
+ var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5);
+ ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
+ ctx.fillStyle = this.pointLabelFontColor;
+
+ var labelsCount = this.labels.length,
+ halfLabelsCount = this.labels.length/2,
+ quarterLabelsCount = halfLabelsCount/2,
+ upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount),
+ exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount);
+ if (i === 0){
+ ctx.textAlign = 'center';
+ } else if(i === halfLabelsCount){
+ ctx.textAlign = 'center';
+ } else if (i < halfLabelsCount){
+ ctx.textAlign = 'left';
+ } else {
+ ctx.textAlign = 'right';
+ }
+
+ // Set the correct text baseline based on outer positioning
+ if (exactQuarter){
+ ctx.textBaseline = 'middle';
+ } else if (upperHalf){
+ ctx.textBaseline = 'bottom';
+ } else {
+ ctx.textBaseline = 'top';
+ }
+
+ ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y);
+ }
+ }
+ }
+ }
+ });
+
+ // Attach global event to resize each chart instance when the browser resizes
+ helpers.addEvent(window, "resize", (function(){
+ // Basic debounce of resize function so it doesn't hurt performance when resizing browser.
+ var timeout;
+ return function(){
+ clearTimeout(timeout);
+ timeout = setTimeout(function(){
+ each(Chart.instances,function(instance){
+ // If the responsive flag is set in the chart instance config
+ // Cascade the resize event down to the chart.
+ if (instance.options.responsive){
+ instance.resize(instance.render, true);
+ }
+ });
+ }, 50);
+ };
+ })());
+
+
+ if (amd) {
+ define(function(){
+ return Chart;
+ });
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = Chart;
+ }
+
+ root.Chart = Chart;
+
+ Chart.noConflict = function(){
+ root.Chart = previous;
+ return Chart;
+ };
+
+}).call(this);
+
+(function(){
+ "use strict";
+
+ var root = this,
+ Chart = root.Chart,
+ helpers = Chart.helpers;
+
+
+ var defaultConfig = {
+ //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
+ scaleBeginAtZero : true,
+
+ //Boolean - Whether grid lines are shown across the chart
+ scaleShowGridLines : true,
+
+ //String - Colour of the grid lines
+ scaleGridLineColor : "rgba(0,0,0,.05)",
+
+ //Number - Width of the grid lines
+ scaleGridLineWidth : 1,
+
+ //Boolean - Whether to show horizontal lines (except X axis)
+ scaleShowHorizontalLines: true,
+
+ //Boolean - Whether to show vertical lines (except Y axis)
+ scaleShowVerticalLines: true,
+
+ //Boolean - If there is a stroke on each bar
+ barShowStroke : true,
+
+ //Number - Pixel width of the bar stroke
+ barStrokeWidth : 2,
+
+ //Number - Spacing between each of the X value sets
+ barValueSpacing : 5,
+
+ //Number - Spacing between data sets within X values
+ barDatasetSpacing : 1,
+
+ //String - A legend template
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+ };
+
+
+ Chart.Type.extend({
+ name: "Bar",
+ defaults : defaultConfig,
+ initialize: function(data){
+
+ //Expose options as a scope variable here so we can access it in the ScaleClass
+ var options = this.options;
+
+ this.ScaleClass = Chart.Scale.extend({
+ offsetGridLines : true,
+ calculateBarX : function(datasetCount, datasetIndex, barIndex){
+ //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar
+ var xWidth = this.calculateBaseWidth(),
+ xAbsolute = this.calculateX(barIndex) - (xWidth/2),
+ barWidth = this.calculateBarWidth(datasetCount);
+
+ return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2;
+ },
+ calculateBaseWidth : function(){
+ return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing);
+ },
+ calculateBarWidth : function(datasetCount){
+ //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
+ var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing);
+
+ return (baseWidth / datasetCount);
+ }
+ });
+
+ this.datasets = [];
+
+ //Set up tooltip events on the chart
+ if (this.options.showTooltips){
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+ var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];
+
+ this.eachBars(function(bar){
+ bar.restore(['fillColor', 'strokeColor']);
+ });
+ helpers.each(activeBars, function(activeBar){
+ activeBar.fillColor = activeBar.highlightFill;
+ activeBar.strokeColor = activeBar.highlightStroke;
+ });
+ this.showTooltip(activeBars);
+ });
+ }
+
+ //Declare the extension of the default point, to cater for the options passed in to the constructor
+ this.BarClass = Chart.Rectangle.extend({
+ strokeWidth : this.options.barStrokeWidth,
+ showStroke : this.options.barShowStroke,
+ ctx : this.chart.ctx
+ });
+
+ //Iterate through each of the datasets, and build this into a property of the chart
+ helpers.each(data.datasets,function(dataset,datasetIndex){
+
+ var datasetObject = {
+ label : dataset.label || null,
+ fillColor : dataset.fillColor,
+ strokeColor : dataset.strokeColor,
+ bars : []
+ };
+
+ this.datasets.push(datasetObject);
+
+ helpers.each(dataset.data,function(dataPoint,index){
+ //Add a new point for each piece of data, passing any required data to draw.
+ datasetObject.bars.push(new this.BarClass({
+ value : dataPoint,
+ label : data.labels[index],
+ datasetLabel: dataset.label,
+ strokeColor : dataset.strokeColor,
+ fillColor : dataset.fillColor,
+ highlightFill : dataset.highlightFill || dataset.fillColor,
+ highlightStroke : dataset.highlightStroke || dataset.strokeColor
+ }));
+ },this);
+
+ },this);
+
+ this.buildScale(data.labels);
+
+ this.BarClass.prototype.base = this.scale.endPoint;
+
+ this.eachBars(function(bar, index, datasetIndex){
+ helpers.extend(bar, {
+ width : this.scale.calculateBarWidth(this.datasets.length),
+ x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+ y: this.scale.endPoint
+ });
+ bar.save();
+ }, this);
+
+ this.render();
+ },
+ update : function(){
+ this.scale.update();
+ // Reset any highlight colours before updating.
+ helpers.each(this.activeElements, function(activeElement){
+ activeElement.restore(['fillColor', 'strokeColor']);
+ });
+
+ this.eachBars(function(bar){
+ bar.save();
+ });
+ this.render();
+ },
+ eachBars : function(callback){
+ helpers.each(this.datasets,function(dataset, datasetIndex){
+ helpers.each(dataset.bars, callback, this, datasetIndex);
+ },this);
+ },
+ getBarsAtEvent : function(e){
+ var barsArray = [],
+ eventPosition = helpers.getRelativePosition(e),
+ datasetIterator = function(dataset){
+ barsArray.push(dataset.bars[barIndex]);
+ },
+ barIndex;
+
+ for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) {
+ for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) {
+ if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){
+ helpers.each(this.datasets, datasetIterator);
+ return barsArray;
+ }
+ }
+ }
+
+ return barsArray;
+ },
+ buildScale : function(labels){
+ var self = this;
+
+ var dataTotal = function(){
+ var values = [];
+ self.eachBars(function(bar){
+ values.push(bar.value);
+ });
+ return values;
+ };
+
+ var scaleOptions = {
+ templateString : this.options.scaleLabel,
+ height : this.chart.height,
+ width : this.chart.width,
+ ctx : this.chart.ctx,
+ textColor : this.options.scaleFontColor,
+ fontSize : this.options.scaleFontSize,
+ fontStyle : this.options.scaleFontStyle,
+ fontFamily : this.options.scaleFontFamily,
+ valuesCount : labels.length,
+ beginAtZero : this.options.scaleBeginAtZero,
+ integersOnly : this.options.scaleIntegersOnly,
+ calculateYRange: function(currentHeight){
+ var updatedRanges = helpers.calculateScaleRange(
+ dataTotal(),
+ currentHeight,
+ this.fontSize,
+ this.beginAtZero,
+ this.integersOnly
+ );
+ helpers.extend(this, updatedRanges);
+ },
+ xLabels : labels,
+ font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
+ lineWidth : this.options.scaleLineWidth,
+ lineColor : this.options.scaleLineColor,
+ showHorizontalLines : this.options.scaleShowHorizontalLines,
+ showVerticalLines : this.options.scaleShowVerticalLines,
+ gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+ gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
+ padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0,
+ showLabels : this.options.scaleShowLabels,
+ display : this.options.showScale
+ };
+
+ if (this.options.scaleOverride){
+ helpers.extend(scaleOptions, {
+ calculateYRange: helpers.noop,
+ steps: this.options.scaleSteps,
+ stepValue: this.options.scaleStepWidth,
+ min: this.options.scaleStartValue,
+ max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+ });
+ }
+
+ this.scale = new this.ScaleClass(scaleOptions);
+ },
+ addData : function(valuesArray,label){
+ //Map the values array for each of the datasets
+ helpers.each(valuesArray,function(value,datasetIndex){
+ //Add a new point for each piece of data, passing any required data to draw.
+ this.datasets[datasetIndex].bars.push(new this.BarClass({
+ value : value,
+ label : label,
+ x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1),
+ y: this.scale.endPoint,
+ width : this.scale.calculateBarWidth(this.datasets.length),
+ base : this.scale.endPoint,
+ strokeColor : this.datasets[datasetIndex].strokeColor,
+ fillColor : this.datasets[datasetIndex].fillColor
+ }));
+ },this);
+
+ this.scale.addXLabel(label);
+ //Then re-render the chart.
+ this.update();
+ },
+ removeData : function(){
+ this.scale.removeXLabel();
+ //Then re-render the chart.
+ helpers.each(this.datasets,function(dataset){
+ dataset.bars.shift();
+ },this);
+ this.update();
+ },
+ reflow : function(){
+ helpers.extend(this.BarClass.prototype,{
+ y: this.scale.endPoint,
+ base : this.scale.endPoint
+ });
+ var newScaleProps = helpers.extend({
+ height : this.chart.height,
+ width : this.chart.width
+ });
+ this.scale.update(newScaleProps);
+ },
+ draw : function(ease){
+ var easingDecimal = ease || 1;
+ this.clear();
+
+ var ctx = this.chart.ctx;
+
+ this.scale.draw(easingDecimal);
+
+ //Draw all the bars for each dataset
+ helpers.each(this.datasets,function(dataset,datasetIndex){
+ helpers.each(dataset.bars,function(bar,index){
+ if (bar.hasValue()){
+ bar.base = this.scale.endPoint;
+ //Transition then draw
+ bar.transition({
+ x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+ y : this.scale.calculateY(bar.value),
+ width : this.scale.calculateBarWidth(this.datasets.length)
+ }, easingDecimal).draw();
+ }
+ },this);
+
+ },this);
+ }
+ });
+
+
+}).call(this);
+
+(function(){
+ "use strict";
+
+ var root = this,
+ Chart = root.Chart,
+ //Cache a local reference to Chart.helpers
+ helpers = Chart.helpers;
+
+ var defaultConfig = {
+ //Boolean - Whether we should show a stroke on each segment
+ segmentShowStroke : true,
+
+ //String - The colour of each segment stroke
+ segmentStrokeColor : "#fff",
+
+ //Number - The width of each segment stroke
+ segmentStrokeWidth : 2,
+
+ //The percentage of the chart that we cut out of the middle.
+ percentageInnerCutout : 50,
+
+ //Number - Amount of animation steps
+ animationSteps : 100,
+
+ //String - Animation easing effect
+ animationEasing : "easeOutBounce",
+
+ //Boolean - Whether we animate the rotation of the Doughnut
+ animateRotate : true,
+
+ //Boolean - Whether we animate scaling the Doughnut from the centre
+ animateScale : false,
+
+ //String - A legend template
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
+
+ };
+
+
+ Chart.Type.extend({
+ //Passing in a name registers this chart in the Chart namespace
+ name: "Doughnut",
+ //Providing a defaults will also register the deafults in the chart namespace
+ defaults : defaultConfig,
+ //Initialize is fired when the chart is initialized - Data is passed in as a parameter
+ //Config is automatically merged by the core of Chart.js, and is available at this.options
+ initialize: function(data){
+
+ //Declare segments as a static property to prevent inheriting across the Chart type prototype
+ this.segments = [];
+ this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2;
+
+ this.SegmentArc = Chart.Arc.extend({
+ ctx : this.chart.ctx,
+ x : this.chart.width/2,
+ y : this.chart.height/2
+ });
+
+ //Set up tooltip events on the chart
+ if (this.options.showTooltips){
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+ var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
+
+ helpers.each(this.segments,function(segment){
+ segment.restore(["fillColor"]);
+ });
+ helpers.each(activeSegments,function(activeSegment){
+ activeSegment.fillColor = activeSegment.highlightColor;
+ });
+ this.showTooltip(activeSegments);
+ });
+ }
+ this.calculateTotal(data);
+
+ helpers.each(data,function(datapoint, index){
+ this.addData(datapoint, index, true);
+ },this);
+
+ this.render();
+ },
+ getSegmentsAtEvent : function(e){
+ var segmentsArray = [];
+
+ var location = helpers.getRelativePosition(e);
+
+ helpers.each(this.segments,function(segment){
+ if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
+ },this);
+ return segmentsArray;
+ },
+ addData : function(segment, atIndex, silent){
+ var index = atIndex || this.segments.length;
+ this.segments.splice(index, 0, new this.SegmentArc({
+ value : segment.value,
+ outerRadius : (this.options.animateScale) ? 0 : this.outerRadius,
+ innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout,
+ fillColor : segment.color,
+ highlightColor : segment.highlight || segment.color,
+ showStroke : this.options.segmentShowStroke,
+ strokeWidth : this.options.segmentStrokeWidth,
+ strokeColor : this.options.segmentStrokeColor,
+ startAngle : Math.PI * 1.5,
+ circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value),
+ label : segment.label
+ }));
+ if (!silent){
+ this.reflow();
+ this.update();
+ }
+ },
+ calculateCircumference : function(value){
+ return (Math.PI*2)*(Math.abs(value) / this.total);
+ },
+ calculateTotal : function(data){
+ this.total = 0;
+ helpers.each(data,function(segment){
+ this.total += Math.abs(segment.value);
+ },this);
+ },
+ update : function(){
+ this.calculateTotal(this.segments);
+
+ // Reset any highlight colours before updating.
+ helpers.each(this.activeElements, function(activeElement){
+ activeElement.restore(['fillColor']);
+ });
+
+ helpers.each(this.segments,function(segment){
+ segment.save();
+ });
+ this.render();
+ },
+
+ removeData: function(atIndex){
+ var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
+ this.segments.splice(indexToDelete, 1);
+ this.reflow();
+ this.update();
+ },
+
+ reflow : function(){
+ helpers.extend(this.SegmentArc.prototype,{
+ x : this.chart.width/2,
+ y : this.chart.height/2
+ });
+ this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2;
+ helpers.each(this.segments, function(segment){
+ segment.update({
+ outerRadius : this.outerRadius,
+ innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
+ });
+ }, this);
+ },
+ draw : function(easeDecimal){
+ var animDecimal = (easeDecimal) ? easeDecimal : 1;
+ this.clear();
+ helpers.each(this.segments,function(segment,index){
+ segment.transition({
+ circumference : this.calculateCircumference(segment.value),
+ outerRadius : this.outerRadius,
+ innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
+ },animDecimal);
+
+ segment.endAngle = segment.startAngle + segment.circumference;
+
+ segment.draw();
+ if (index === 0){
+ segment.startAngle = Math.PI * 1.5;
+ }
+ //Check to see if it's the last segment, if not get the next and update the start angle
+ if (index < this.segments.length-1){
+ this.segments[index+1].startAngle = segment.endAngle;
+ }
+ },this);
+
+ }
+ });
+
+ Chart.types.Doughnut.extend({
+ name : "Pie",
+ defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0})
+ });
+
+}).call(this);
+(function(){
+ "use strict";
+
+ var root = this,
+ Chart = root.Chart,
+ helpers = Chart.helpers;
+
+ var defaultConfig = {
+
+ ///Boolean - Whether grid lines are shown across the chart
+ scaleShowGridLines : true,
+
+ //String - Colour of the grid lines
+ scaleGridLineColor : "rgba(0,0,0,.05)",
+
+ //Number - Width of the grid lines
+ scaleGridLineWidth : 1,
+
+ //Boolean - Whether to show horizontal lines (except X axis)
+ scaleShowHorizontalLines: true,
+
+ //Boolean - Whether to show vertical lines (except Y axis)
+ scaleShowVerticalLines: true,
+
+ //Boolean - Whether the line is curved between points
+ bezierCurve : true,
+
+ //Number - Tension of the bezier curve between points
+ bezierCurveTension : 0.4,
+
+ //Boolean - Whether to show a dot for each point
+ pointDot : true,
+
+ //Number - Radius of each point dot in pixels
+ pointDotRadius : 4,
+
+ //Number - Pixel width of point dot stroke
+ pointDotStrokeWidth : 1,
+
+ //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
+ pointHitDetectionRadius : 20,
+
+ //Boolean - Whether to show a stroke for datasets
+ datasetStroke : true,
+
+ //Number - Pixel width of dataset stroke
+ datasetStrokeWidth : 2,
+
+ //Boolean - Whether to fill the dataset with a colour
+ datasetFill : true,
+
+ //String - A legend template
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+ };
+
+
+ Chart.Type.extend({
+ name: "Line",
+ defaults : defaultConfig,
+ initialize: function(data){
+ //Declare the extension of the default point, to cater for the options passed in to the constructor
+ this.PointClass = Chart.Point.extend({
+ strokeWidth : this.options.pointDotStrokeWidth,
+ radius : this.options.pointDotRadius,
+ display: this.options.pointDot,
+ hitDetectionRadius : this.options.pointHitDetectionRadius,
+ ctx : this.chart.ctx,
+ inRange : function(mouseX){
+ return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2));
+ }
+ });
+
+ this.datasets = [];
+
+ //Set up tooltip events on the chart
+ if (this.options.showTooltips){
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+ var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
+ this.eachPoints(function(point){
+ point.restore(['fillColor', 'strokeColor']);
+ });
+ helpers.each(activePoints, function(activePoint){
+ activePoint.fillColor = activePoint.highlightFill;
+ activePoint.strokeColor = activePoint.highlightStroke;
+ });
+ this.showTooltip(activePoints);
+ });
+ }
+
+ //Iterate through each of the datasets, and build this into a property of the chart
+ helpers.each(data.datasets,function(dataset){
+
+ var datasetObject = {
+ label : dataset.label || null,
+ fillColor : dataset.fillColor,
+ strokeColor : dataset.strokeColor,
+ pointColor : dataset.pointColor,
+ pointStrokeColor : dataset.pointStrokeColor,
+ points : []
+ };
+
+ this.datasets.push(datasetObject);
+
+
+ helpers.each(dataset.data,function(dataPoint,index){
+ //Add a new point for each piece of data, passing any required data to draw.
+ datasetObject.points.push(new this.PointClass({
+ value : dataPoint,
+ label : data.labels[index],
+ datasetLabel: dataset.label,
+ strokeColor : dataset.pointStrokeColor,
+ fillColor : dataset.pointColor,
+ highlightFill : dataset.pointHighlightFill || dataset.pointColor,
+ highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
+ }));
+ },this);
+
+ this.buildScale(data.labels);
+
+
+ this.eachPoints(function(point, index){
+ helpers.extend(point, {
+ x: this.scale.calculateX(index),
+ y: this.scale.endPoint
+ });
+ point.save();
+ }, this);
+
+ },this);
+
+
+ this.render();
+ },
+ update : function(){
+ this.scale.update();
+ // Reset any highlight colours before updating.
+ helpers.each(this.activeElements, function(activeElement){
+ activeElement.restore(['fillColor', 'strokeColor']);
+ });
+ this.eachPoints(function(point){
+ point.save();
+ });
+ this.render();
+ },
+ eachPoints : function(callback){
+ helpers.each(this.datasets,function(dataset){
+ helpers.each(dataset.points,callback,this);
+ },this);
+ },
+ getPointsAtEvent : function(e){
+ var pointsArray = [],
+ eventPosition = helpers.getRelativePosition(e);
+ helpers.each(this.datasets,function(dataset){
+ helpers.each(dataset.points,function(point){
+ if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point);
+ });
+ },this);
+ return pointsArray;
+ },
+ buildScale : function(labels){
+ var self = this;
+
+ var dataTotal = function(){
+ var values = [];
+ self.eachPoints(function(point){
+ values.push(point.value);
+ });
+
+ return values;
+ };
+
+ var scaleOptions = {
+ templateString : this.options.scaleLabel,
+ height : this.chart.height,
+ width : this.chart.width,
+ ctx : this.chart.ctx,
+ textColor : this.options.scaleFontColor,
+ fontSize : this.options.scaleFontSize,
+ fontStyle : this.options.scaleFontStyle,
+ fontFamily : this.options.scaleFontFamily,
+ valuesCount : labels.length,
+ beginAtZero : this.options.scaleBeginAtZero,
+ integersOnly : this.options.scaleIntegersOnly,
+ calculateYRange : function(currentHeight){
+ var updatedRanges = helpers.calculateScaleRange(
+ dataTotal(),
+ currentHeight,
+ this.fontSize,
+ this.beginAtZero,
+ this.integersOnly
+ );
+ helpers.extend(this, updatedRanges);
+ },
+ xLabels : labels,
+ font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
+ lineWidth : this.options.scaleLineWidth,
+ lineColor : this.options.scaleLineColor,
+ showHorizontalLines : this.options.scaleShowHorizontalLines,
+ showVerticalLines : this.options.scaleShowVerticalLines,
+ gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+ gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
+ padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
+ showLabels : this.options.scaleShowLabels,
+ display : this.options.showScale
+ };
+
+ if (this.options.scaleOverride){
+ helpers.extend(scaleOptions, {
+ calculateYRange: helpers.noop,
+ steps: this.options.scaleSteps,
+ stepValue: this.options.scaleStepWidth,
+ min: this.options.scaleStartValue,
+ max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+ });
+ }
+
+
+ this.scale = new Chart.Scale(scaleOptions);
+ },
+ addData : function(valuesArray,label){
+ //Map the values array for each of the datasets
+
+ helpers.each(valuesArray,function(value,datasetIndex){
+ //Add a new point for each piece of data, passing any required data to draw.
+ this.datasets[datasetIndex].points.push(new this.PointClass({
+ value : value,
+ label : label,
+ x: this.scale.calculateX(this.scale.valuesCount+1),
+ y: this.scale.endPoint,
+ strokeColor : this.datasets[datasetIndex].pointStrokeColor,
+ fillColor : this.datasets[datasetIndex].pointColor
+ }));
+ },this);
+
+ this.scale.addXLabel(label);
+ //Then re-render the chart.
+ this.update();
+ },
+ removeData : function(){
+ this.scale.removeXLabel();
+ //Then re-render the chart.
+ helpers.each(this.datasets,function(dataset){
+ dataset.points.shift();
+ },this);
+ this.update();
+ },
+ reflow : function(){
+ var newScaleProps = helpers.extend({
+ height : this.chart.height,
+ width : this.chart.width
+ });
+ this.scale.update(newScaleProps);
+ },
+ draw : function(ease){
+ var easingDecimal = ease || 1;
+ this.clear();
+
+ var ctx = this.chart.ctx;
+
+ // Some helper methods for getting the next/prev points
+ var hasValue = function(item){
+ return item.value !== null;
+ },
+ nextPoint = function(point, collection, index){
+ return helpers.findNextWhere(collection, hasValue, index) || point;
+ },
+ previousPoint = function(point, collection, index){
+ return helpers.findPreviousWhere(collection, hasValue, index) || point;
+ };
+
+ this.scale.draw(easingDecimal);
+
+
+ helpers.each(this.datasets,function(dataset){
+ var pointsWithValues = helpers.where(dataset.points, hasValue);
+
+ //Transition each point first so that the line and point drawing isn't out of sync
+ //We can use this extra loop to calculate the control points of this dataset also in this loop
+
+ helpers.each(dataset.points, function(point, index){
+ if (point.hasValue()){
+ point.transition({
+ y : this.scale.calculateY(point.value),
+ x : this.scale.calculateX(index)
+ }, easingDecimal);
+ }
+ },this);
+
+
+ // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point
+ // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed
+ if (this.options.bezierCurve){
+ helpers.each(pointsWithValues, function(point, index){
+ var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0;
+ point.controlPoints = helpers.splineCurve(
+ previousPoint(point, pointsWithValues, index),
+ point,
+ nextPoint(point, pointsWithValues, index),
+ tension
+ );
+
+ // Prevent the bezier going outside of the bounds of the graph
+
+ // Cap puter bezier handles to the upper/lower scale bounds
+ if (point.controlPoints.outer.y > this.scale.endPoint){
+ point.controlPoints.outer.y = this.scale.endPoint;
+ }
+ else if (point.controlPoints.outer.y < this.scale.startPoint){
+ point.controlPoints.outer.y = this.scale.startPoint;
+ }
+
+ // Cap inner bezier handles to the upper/lower scale bounds
+ if (point.controlPoints.inner.y > this.scale.endPoint){
+ point.controlPoints.inner.y = this.scale.endPoint;
+ }
+ else if (point.controlPoints.inner.y < this.scale.startPoint){
+ point.controlPoints.inner.y = this.scale.startPoint;
+ }
+ },this);
+ }
+
+
+ //Draw the line between all the points
+ ctx.lineWidth = this.options.datasetStrokeWidth;
+ ctx.strokeStyle = dataset.strokeColor;
+ ctx.beginPath();
+
+ helpers.each(pointsWithValues, function(point, index){
+ if (index === 0){
+ ctx.moveTo(point.x, point.y);
+ }
+ else{
+ if(this.options.bezierCurve){
+ var previous = previousPoint(point, pointsWithValues, index);
+
+ ctx.bezierCurveTo(
+ previous.controlPoints.outer.x,
+ previous.controlPoints.outer.y,
+ point.controlPoints.inner.x,
+ point.controlPoints.inner.y,
+ point.x,
+ point.y
+ );
+ }
+ else{
+ ctx.lineTo(point.x,point.y);
+ }
+ }
+ }, this);
+
+ ctx.stroke();
+
+ if (this.options.datasetFill && pointsWithValues.length > 0){
+ //Round off the line by going to the base of the chart, back to the start, then fill.
+ ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint);
+ ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint);
+ ctx.fillStyle = dataset.fillColor;
+ ctx.closePath();
+ ctx.fill();
+ }
+
+ //Now draw the points over the line
+ //A little inefficient double looping, but better than the line
+ //lagging behind the point positions
+ helpers.each(pointsWithValues,function(point){
+ point.draw();
+ });
+ },this);
+ }
+ });
+
+
+}).call(this);
+
+(function(){
+ "use strict";
+
+ var root = this,
+ Chart = root.Chart,
+ //Cache a local reference to Chart.helpers
+ helpers = Chart.helpers;
+
+ var defaultConfig = {
+ //Boolean - Show a backdrop to the scale label
+ scaleShowLabelBackdrop : true,
+
+ //String - The colour of the label backdrop
+ scaleBackdropColor : "rgba(255,255,255,0.75)",
+
+ // Boolean - Whether the scale should begin at zero
+ scaleBeginAtZero : true,
+
+ //Number - The backdrop padding above & below the label in pixels
+ scaleBackdropPaddingY : 2,
+
+ //Number - The backdrop padding to the side of the label in pixels
+ scaleBackdropPaddingX : 2,
+
+ //Boolean - Show line for each value in the scale
+ scaleShowLine : true,
+
+ //Boolean - Stroke a line around each segment in the chart
+ segmentShowStroke : true,
+
+ //String - The colour of the stroke on each segement.
+ segmentStrokeColor : "#fff",
+
+ //Number - The width of the stroke value in pixels
+ segmentStrokeWidth : 2,
+
+ //Number - Amount of animation steps
+ animationSteps : 100,
+
+ //String - Animation easing effect.
+ animationEasing : "easeOutBounce",
+
+ //Boolean - Whether to animate the rotation of the chart
+ animateRotate : true,
+
+ //Boolean - Whether to animate scaling the chart from the centre
+ animateScale : false,
+
+ //String - A legend template
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
+ };
+
+
+ Chart.Type.extend({
+ //Passing in a name registers this chart in the Chart namespace
+ name: "PolarArea",
+ //Providing a defaults will also register the deafults in the chart namespace
+ defaults : defaultConfig,
+ //Initialize is fired when the chart is initialized - Data is passed in as a parameter
+ //Config is automatically merged by the core of Chart.js, and is available at this.options
+ initialize: function(data){
+ this.segments = [];
+ //Declare segment class as a chart instance specific class, so it can share props for this instance
+ this.SegmentArc = Chart.Arc.extend({
+ showStroke : this.options.segmentShowStroke,
+ strokeWidth : this.options.segmentStrokeWidth,
+ strokeColor : this.options.segmentStrokeColor,
+ ctx : this.chart.ctx,
+ innerRadius : 0,
+ x : this.chart.width/2,
+ y : this.chart.height/2
+ });
+ this.scale = new Chart.RadialScale({
+ display: this.options.showScale,
+ fontStyle: this.options.scaleFontStyle,
+ fontSize: this.options.scaleFontSize,
+ fontFamily: this.options.scaleFontFamily,
+ fontColor: this.options.scaleFontColor,
+ showLabels: this.options.scaleShowLabels,
+ showLabelBackdrop: this.options.scaleShowLabelBackdrop,
+ backdropColor: this.options.scaleBackdropColor,
+ backdropPaddingY : this.options.scaleBackdropPaddingY,
+ backdropPaddingX: this.options.scaleBackdropPaddingX,
+ lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
+ lineColor: this.options.scaleLineColor,
+ lineArc: true,
+ width: this.chart.width,
+ height: this.chart.height,
+ xCenter: this.chart.width/2,
+ yCenter: this.chart.height/2,
+ ctx : this.chart.ctx,
+ templateString: this.options.scaleLabel,
+ valuesCount: data.length
+ });
+
+ this.updateScaleRange(data);
+
+ this.scale.update();
+
+ helpers.each(data,function(segment,index){
+ this.addData(segment,index,true);
+ },this);
+
+ //Set up tooltip events on the chart
+ if (this.options.showTooltips){
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+ var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
+ helpers.each(this.segments,function(segment){
+ segment.restore(["fillColor"]);
+ });
+ helpers.each(activeSegments,function(activeSegment){
+ activeSegment.fillColor = activeSegment.highlightColor;
+ });
+ this.showTooltip(activeSegments);
+ });
+ }
+
+ this.render();
+ },
+ getSegmentsAtEvent : function(e){
+ var segmentsArray = [];
+
+ var location = helpers.getRelativePosition(e);
+
+ helpers.each(this.segments,function(segment){
+ if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
+ },this);
+ return segmentsArray;
+ },
+ addData : function(segment, atIndex, silent){
+ var index = atIndex || this.segments.length;
+
+ this.segments.splice(index, 0, new this.SegmentArc({
+ fillColor: segment.color,
+ highlightColor: segment.highlight || segment.color,
+ label: segment.label,
+ value: segment.value,
+ outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value),
+ circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(),
+ startAngle: Math.PI * 1.5
+ }));
+ if (!silent){
+ this.reflow();
+ this.update();
+ }
+ },
+ removeData: function(atIndex){
+ var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
+ this.segments.splice(indexToDelete, 1);
+ this.reflow();
+ this.update();
+ },
+ calculateTotal: function(data){
+ this.total = 0;
+ helpers.each(data,function(segment){
+ this.total += segment.value;
+ },this);
+ this.scale.valuesCount = this.segments.length;
+ },
+ updateScaleRange: function(datapoints){
+ var valuesArray = [];
+ helpers.each(datapoints,function(segment){
+ valuesArray.push(segment.value);
+ });
+
+ var scaleSizes = (this.options.scaleOverride) ?
+ {
+ steps: this.options.scaleSteps,
+ stepValue: this.options.scaleStepWidth,
+ min: this.options.scaleStartValue,
+ max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+ } :
+ helpers.calculateScaleRange(
+ valuesArray,
+ helpers.min([this.chart.width, this.chart.height])/2,
+ this.options.scaleFontSize,
+ this.options.scaleBeginAtZero,
+ this.options.scaleIntegersOnly
+ );
+
+ helpers.extend(
+ this.scale,
+ scaleSizes,
+ {
+ size: helpers.min([this.chart.width, this.chart.height]),
+ xCenter: this.chart.width/2,
+ yCenter: this.chart.height/2
+ }
+ );
+
+ },
+ update : function(){
+ this.calculateTotal(this.segments);
+
+ helpers.each(this.segments,function(segment){
+ segment.save();
+ });
+
+ this.reflow();
+ this.render();
+ },
+ reflow : function(){
+ helpers.extend(this.SegmentArc.prototype,{
+ x : this.chart.width/2,
+ y : this.chart.height/2
+ });
+ this.updateScaleRange(this.segments);
+ this.scale.update();
+
+ helpers.extend(this.scale,{
+ xCenter: this.chart.width/2,
+ yCenter: this.chart.height/2
+ });
+
+ helpers.each(this.segments, function(segment){
+ segment.update({
+ outerRadius : this.scale.calculateCenterOffset(segment.value)
+ });
+ }, this);
+
+ },
+ draw : function(ease){
+ var easingDecimal = ease || 1;
+ //Clear & draw the canvas
+ this.clear();
+ helpers.each(this.segments,function(segment, index){
+ segment.transition({
+ circumference : this.scale.getCircumference(),
+ outerRadius : this.scale.calculateCenterOffset(segment.value)
+ },easingDecimal);
+
+ segment.endAngle = segment.startAngle + segment.circumference;
+
+ // If we've removed the first segment we need to set the first one to
+ // start at the top.
+ if (index === 0){
+ segment.startAngle = Math.PI * 1.5;
+ }
+
+ //Check to see if it's the last segment, if not get the next and update the start angle
+ if (index < this.segments.length - 1){
+ this.segments[index+1].startAngle = segment.endAngle;
+ }
+ segment.draw();
+ }, this);
+ this.scale.draw();
+ }
+ });
+
+}).call(this);
+(function(){
+ "use strict";
+
+ var root = this,
+ Chart = root.Chart,
+ helpers = Chart.helpers;
+
+
+
+ Chart.Type.extend({
+ name: "Radar",
+ defaults:{
+ //Boolean - Whether to show lines for each scale point
+ scaleShowLine : true,
+
+ //Boolean - Whether we show the angle lines out of the radar
+ angleShowLineOut : true,
+
+ //Boolean - Whether to show labels on the scale
+ scaleShowLabels : false,
+
+ // Boolean - Whether the scale should begin at zero
+ scaleBeginAtZero : true,
+
+ //String - Colour of the angle line
+ angleLineColor : "rgba(0,0,0,.1)",
+
+ //Number - Pixel width of the angle line
+ angleLineWidth : 1,
+
+ //String - Point label font declaration
+ pointLabelFontFamily : "'Arial'",
+
+ //String - Point label font weight
+ pointLabelFontStyle : "normal",
+
+ //Number - Point label font size in pixels
+ pointLabelFontSize : 10,
+
+ //String - Point label font colour
+ pointLabelFontColor : "#666",
+
+ //Boolean - Whether to show a dot for each point
+ pointDot : true,
+
+ //Number - Radius of each point dot in pixels
+ pointDotRadius : 3,
+
+ //Number - Pixel width of point dot stroke
+ pointDotStrokeWidth : 1,
+
+ //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
+ pointHitDetectionRadius : 20,
+
+ //Boolean - Whether to show a stroke for datasets
+ datasetStroke : true,
+
+ //Number - Pixel width of dataset stroke
+ datasetStrokeWidth : 2,
+
+ //Boolean - Whether to fill the dataset with a colour
+ datasetFill : true,
+
+ //String - A legend template
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+ },
+
+ initialize: function(data){
+ this.PointClass = Chart.Point.extend({
+ strokeWidth : this.options.pointDotStrokeWidth,
+ radius : this.options.pointDotRadius,
+ display: this.options.pointDot,
+ hitDetectionRadius : this.options.pointHitDetectionRadius,
+ ctx : this.chart.ctx
+ });
+
+ this.datasets = [];
+
+ this.buildScale(data);
+
+ //Set up tooltip events on the chart
+ if (this.options.showTooltips){
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+ var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
+
+ this.eachPoints(function(point){
+ point.restore(['fillColor', 'strokeColor']);
+ });
+ helpers.each(activePointsCollection, function(activePoint){
+ activePoint.fillColor = activePoint.highlightFill;
+ activePoint.strokeColor = activePoint.highlightStroke;
+ });
+
+ this.showTooltip(activePointsCollection);
+ });
+ }
+
+ //Iterate through each of the datasets, and build this into a property of the chart
+ helpers.each(data.datasets,function(dataset){
+
+ var datasetObject = {
+ label: dataset.label || null,
+ fillColor : dataset.fillColor,
+ strokeColor : dataset.strokeColor,
+ pointColor : dataset.pointColor,
+ pointStrokeColor : dataset.pointStrokeColor,
+ points : []
+ };
+
+ this.datasets.push(datasetObject);
+
+ helpers.each(dataset.data,function(dataPoint,index){
+ //Add a new point for each piece of data, passing any required data to draw.
+ var pointPosition;
+ if (!this.scale.animation){
+ pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint));
+ }
+ datasetObject.points.push(new this.PointClass({
+ value : dataPoint,
+ label : data.labels[index],
+ datasetLabel: dataset.label,
+ x: (this.options.animation) ? this.scale.xCenter : pointPosition.x,
+ y: (this.options.animation) ? this.scale.yCenter : pointPosition.y,
+ strokeColor : dataset.pointStrokeColor,
+ fillColor : dataset.pointColor,
+ highlightFill : dataset.pointHighlightFill || dataset.pointColor,
+ highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
+ }));
+ },this);
+
+ },this);
+
+ this.render();
+ },
+ eachPoints : function(callback){
+ helpers.each(this.datasets,function(dataset){
+ helpers.each(dataset.points,callback,this);
+ },this);
+ },
+
+ getPointsAtEvent : function(evt){
+ var mousePosition = helpers.getRelativePosition(evt),
+ fromCenter = helpers.getAngleFromPoint({
+ x: this.scale.xCenter,
+ y: this.scale.yCenter
+ }, mousePosition);
+
+ var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount,
+ pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex),
+ activePointsCollection = [];
+
+ // If we're at the top, make the pointIndex 0 to get the first of the array.
+ if (pointIndex >= this.scale.valuesCount || pointIndex < 0){
+ pointIndex = 0;
+ }
+
+ if (fromCenter.distance <= this.scale.drawingArea){
+ helpers.each(this.datasets, function(dataset){
+ activePointsCollection.push(dataset.points[pointIndex]);
+ });
+ }
+
+ return activePointsCollection;
+ },
+
+ buildScale : function(data){
+ this.scale = new Chart.RadialScale({
+ display: this.options.showScale,
+ fontStyle: this.options.scaleFontStyle,
+ fontSize: this.options.scaleFontSize,
+ fontFamily: this.options.scaleFontFamily,
+ fontColor: this.options.scaleFontColor,
+ showLabels: this.options.scaleShowLabels,
+ showLabelBackdrop: this.options.scaleShowLabelBackdrop,
+ backdropColor: this.options.scaleBackdropColor,
+ backdropPaddingY : this.options.scaleBackdropPaddingY,
+ backdropPaddingX: this.options.scaleBackdropPaddingX,
+ lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
+ lineColor: this.options.scaleLineColor,
+ angleLineColor : this.options.angleLineColor,
+ angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0,
+ // Point labels at the edge of each line
+ pointLabelFontColor : this.options.pointLabelFontColor,
+ pointLabelFontSize : this.options.pointLabelFontSize,
+ pointLabelFontFamily : this.options.pointLabelFontFamily,
+ pointLabelFontStyle : this.options.pointLabelFontStyle,
+ height : this.chart.height,
+ width: this.chart.width,
+ xCenter: this.chart.width/2,
+ yCenter: this.chart.height/2,
+ ctx : this.chart.ctx,
+ templateString: this.options.scaleLabel,
+ labels: data.labels,
+ valuesCount: data.datasets[0].data.length
+ });
+
+ this.scale.setScaleSize();
+ this.updateScaleRange(data.datasets);
+ this.scale.buildYLabels();
+ },
+ updateScaleRange: function(datasets){
+ var valuesArray = (function(){
+ var totalDataArray = [];
+ helpers.each(datasets,function(dataset){
+ if (dataset.data){
+ totalDataArray = totalDataArray.concat(dataset.data);
+ }
+ else {
+ helpers.each(dataset.points, function(point){
+ totalDataArray.push(point.value);
+ });
+ }
+ });
+ return totalDataArray;
+ })();
+
+
+ var scaleSizes = (this.options.scaleOverride) ?
+ {
+ steps: this.options.scaleSteps,
+ stepValue: this.options.scaleStepWidth,
+ min: this.options.scaleStartValue,
+ max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+ } :
+ helpers.calculateScaleRange(
+ valuesArray,
+ helpers.min([this.chart.width, this.chart.height])/2,
+ this.options.scaleFontSize,
+ this.options.scaleBeginAtZero,
+ this.options.scaleIntegersOnly
+ );
+
+ helpers.extend(
+ this.scale,
+ scaleSizes
+ );
+
+ },
+ addData : function(valuesArray,label){
+ //Map the values array for each of the datasets
+ this.scale.valuesCount++;
+ helpers.each(valuesArray,function(value,datasetIndex){
+ var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value));
+ this.datasets[datasetIndex].points.push(new this.PointClass({
+ value : value,
+ label : label,
+ x: pointPosition.x,
+ y: pointPosition.y,
+ strokeColor : this.datasets[datasetIndex].pointStrokeColor,
+ fillColor : this.datasets[datasetIndex].pointColor
+ }));
+ },this);
+
+ this.scale.labels.push(label);
+
+ this.reflow();
+
+ this.update();
+ },
+ removeData : function(){
+ this.scale.valuesCount--;
+ this.scale.labels.shift();
+ helpers.each(this.datasets,function(dataset){
+ dataset.points.shift();
+ },this);
+ this.reflow();
+ this.update();
+ },
+ update : function(){
+ this.eachPoints(function(point){
+ point.save();
+ });
+ this.reflow();
+ this.render();
+ },
+ reflow: function(){
+ helpers.extend(this.scale, {
+ width : this.chart.width,
+ height: this.chart.height,
+ size : helpers.min([this.chart.width, this.chart.height]),
+ xCenter: this.chart.width/2,
+ yCenter: this.chart.height/2
+ });
+ this.updateScaleRange(this.datasets);
+ this.scale.setScaleSize();
+ this.scale.buildYLabels();
+ },
+ draw : function(ease){
+ var easeDecimal = ease || 1,
+ ctx = this.chart.ctx;
+ this.clear();
+ this.scale.draw();
+
+ helpers.each(this.datasets,function(dataset){
+
+ //Transition each point first so that the line and point drawing isn't out of sync
+ helpers.each(dataset.points,function(point,index){
+ if (point.hasValue()){
+ point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal);
+ }
+ },this);
+
+
+
+ //Draw the line between all the points
+ ctx.lineWidth = this.options.datasetStrokeWidth;
+ ctx.strokeStyle = dataset.strokeColor;
+ ctx.beginPath();
+ helpers.each(dataset.points,function(point,index){
+ if (index === 0){
+ ctx.moveTo(point.x,point.y);
+ }
+ else{
+ ctx.lineTo(point.x,point.y);
+ }
+ },this);
+ ctx.closePath();
+ ctx.stroke();
+
+ ctx.fillStyle = dataset.fillColor;
+ ctx.fill();
+
+ //Now draw the points over the line
+ //A little inefficient double looping, but better than the line
+ //lagging behind the point positions
+ helpers.each(dataset.points,function(point){
+ if (point.hasValue()){
+ point.draw();
+ }
+ });
+
+ },this);
+
+ }
+
+ });
+
+
+
+
+
+}).call(this); \ No newline at end of file
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
new file mode 100755
index 00000000000..cfa49e72c50
--- /dev/null
+++ b/vendor/assets/javascripts/autosize.js
@@ -0,0 +1,243 @@
+/*!
+ Autosize 3.0.14
+ license: MIT
+ http://www.jacklmoore.com/autosize
+*/
+(function (global, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define(['exports', 'module'], factory);
+ } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
+ factory(exports, module);
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod.exports, mod);
+ global.autosize = mod.exports;
+ }
+})(this, function (exports, module) {
+ 'use strict';
+
+ var set = typeof Set === 'function' ? new Set() : (function () {
+ var list = [];
+
+ return {
+ has: function has(key) {
+ return Boolean(list.indexOf(key) > -1);
+ },
+ add: function add(key) {
+ list.push(key);
+ },
+ 'delete': function _delete(key) {
+ list.splice(list.indexOf(key), 1);
+ } };
+ })();
+
+ function assign(ta) {
+ var _ref = arguments[1] === undefined ? {} : arguments[1];
+
+ var _ref$setOverflowX = _ref.setOverflowX;
+ var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
+ var _ref$setOverflowY = _ref.setOverflowY;
+ var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
+
+ if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
+
+ var heightOffset = null;
+ var overflowY = null;
+ var clientWidth = ta.clientWidth;
+
+ function init() {
+ var style = window.getComputedStyle(ta, null);
+
+ overflowY = style.overflowY;
+
+ if (style.resize === 'vertical') {
+ ta.style.resize = 'none';
+ } else if (style.resize === 'both') {
+ ta.style.resize = 'horizontal';
+ }
+
+ if (style.boxSizing === 'content-box') {
+ heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
+ } else {
+ heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
+ }
+ // Fix when a textarea is not on document body and heightOffset is Not a Number
+ if (isNaN(heightOffset)) {
+ heightOffset = 0;
+ }
+
+ update();
+ }
+
+ function changeOverflow(value) {
+ {
+ // Chrome/Safari-specific fix:
+ // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
+ // made available by removing the scrollbar. The following forces the necessary text reflow.
+ var width = ta.style.width;
+ ta.style.width = '0px';
+ // Force reflow:
+ /* jshint ignore:start */
+ ta.offsetWidth;
+ /* jshint ignore:end */
+ ta.style.width = width;
+ }
+
+ overflowY = value;
+
+ if (setOverflowY) {
+ ta.style.overflowY = value;
+ }
+
+ resize();
+ }
+
+ function resize() {
+ var htmlTop = window.pageYOffset;
+ var bodyTop = document.body.scrollTop;
+ var originalHeight = ta.style.height;
+
+ ta.style.height = 'auto';
+
+ var endHeight = ta.scrollHeight + heightOffset;
+
+ if (ta.scrollHeight === 0) {
+ // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
+ ta.style.height = originalHeight;
+ return;
+ }
+
+ ta.style.height = endHeight + 'px';
+
+ // used to check if an update is actually necessary on window.resize
+ clientWidth = ta.clientWidth;
+
+ // prevents scroll-position jumping
+ document.documentElement.scrollTop = htmlTop;
+ document.body.scrollTop = bodyTop;
+ }
+
+ function update() {
+ var startHeight = ta.style.height;
+
+ resize();
+
+ var style = window.getComputedStyle(ta, null);
+
+ if (style.height !== ta.style.height) {
+ if (overflowY !== 'visible') {
+ changeOverflow('visible');
+ }
+ } else {
+ if (overflowY !== 'hidden') {
+ changeOverflow('hidden');
+ }
+ }
+
+ if (startHeight !== ta.style.height) {
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:resized', true, false);
+ ta.dispatchEvent(evt);
+ }
+ }
+
+ var pageResize = function pageResize() {
+ if (ta.clientWidth !== clientWidth) {
+ update();
+ }
+ };
+
+ var destroy = (function (style) {
+ window.removeEventListener('resize', pageResize, false);
+ ta.removeEventListener('input', update, false);
+ ta.removeEventListener('keyup', update, false);
+ ta.removeEventListener('autosize:destroy', destroy, false);
+ ta.removeEventListener('autosize:update', update, false);
+ set['delete'](ta);
+
+ Object.keys(style).forEach(function (key) {
+ ta.style[key] = style[key];
+ });
+ }).bind(ta, {
+ height: ta.style.height,
+ resize: ta.style.resize,
+ overflowY: ta.style.overflowY,
+ overflowX: ta.style.overflowX,
+ wordWrap: ta.style.wordWrap });
+
+ ta.addEventListener('autosize:destroy', destroy, false);
+
+ // IE9 does not fire onpropertychange or oninput for deletions,
+ // so binding to onkeyup to catch most of those events.
+ // There is no way that I know of to detect something like 'cut' in IE9.
+ if ('onpropertychange' in ta && 'oninput' in ta) {
+ ta.addEventListener('keyup', update, false);
+ }
+
+ window.addEventListener('resize', pageResize, false);
+ ta.addEventListener('input', update, false);
+ ta.addEventListener('autosize:update', update, false);
+ set.add(ta);
+
+ if (setOverflowX) {
+ ta.style.overflowX = 'hidden';
+ ta.style.wordWrap = 'break-word';
+ }
+
+ init();
+ }
+
+ function destroy(ta) {
+ if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:destroy', true, false);
+ ta.dispatchEvent(evt);
+ }
+
+ function update(ta) {
+ if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:update', true, false);
+ ta.dispatchEvent(evt);
+ }
+
+ var autosize = null;
+
+ // Do nothing in Node.js environment and IE8 (or lower)
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
+ autosize = function (el) {
+ return el;
+ };
+ autosize.destroy = function (el) {
+ return el;
+ };
+ autosize.update = function (el) {
+ return el;
+ };
+ } else {
+ autosize = function (el, options) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], function (x) {
+ return assign(x, options);
+ });
+ }
+ return el;
+ };
+ autosize.destroy = function (el) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], destroy);
+ }
+ return el;
+ };
+ autosize.update = function (el) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], update);
+ }
+ return el;
+ };
+ }
+
+ module.exports = autosize;
+}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/chart-lib.min.js b/vendor/assets/javascripts/chart-lib.min.js
deleted file mode 100644
index 3a0a2c87345..00000000000
--- a/vendor/assets/javascripts/chart-lib.min.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/*!
- * Chart.js
- * http://chartjs.org/
- * Version: 1.0.2
- *
- * Copyright 2015 Nick Downie
- * Released under the MIT license
- * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
- */
-(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor}))
-},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file
diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.js b/vendor/assets/javascripts/fuzzaldrin-plus.js
new file mode 100644
index 00000000000..1985e3f8f6c
--- /dev/null
+++ b/vendor/assets/javascripts/fuzzaldrin-plus.js
@@ -0,0 +1,1161 @@
+/*!
+ * fuzzaldrin-plus.js - 0.3.1
+ * https://github.com/jeancroy/fuzzaldrin-plus
+ *
+ * Copyright 2016 - Jean Christophe Roy
+ * Released under the MIT license
+ * https://github.com/jeancroy/fuzzaldrin-plus/raw/master/LICENSE.md
+ */
+(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+fuzzaldrinPlus = require('fuzzaldrin-plus');
+
+},{"fuzzaldrin-plus":3}],2:[function(require,module,exports){
+(function() {
+ var PathSeparator, legacy_scorer, pluckCandidates, scorer, sortCandidates;
+
+ scorer = require('./scorer');
+
+ legacy_scorer = require('./legacy');
+
+ pluckCandidates = function(a) {
+ return a.candidate;
+ };
+
+ sortCandidates = function(a, b) {
+ return b.score - a.score;
+ };
+
+ PathSeparator = require('path').sep;
+
+ module.exports = function(candidates, query, _arg) {
+ var allowErrors, bAllowErrors, bKey, candidate, coreQuery, key, legacy, maxInners, maxResults, prepQuery, queryHasSlashes, score, scoredCandidates, spotLeft, string, _i, _j, _len, _len1, _ref;
+ _ref = _arg != null ? _arg : {}, key = _ref.key, maxResults = _ref.maxResults, maxInners = _ref.maxInners, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
+ scoredCandidates = [];
+ spotLeft = (maxInners != null) && maxInners > 0 ? maxInners : candidates.length;
+ bAllowErrors = !!allowErrors;
+ bKey = key != null;
+ prepQuery = scorer.prepQuery(query);
+ if (!legacy) {
+ for (_i = 0, _len = candidates.length; _i < _len; _i++) {
+ candidate = candidates[_i];
+ string = bKey ? candidate[key] : candidate;
+ if (!string) {
+ continue;
+ }
+ score = scorer.score(string, query, prepQuery, bAllowErrors);
+ if (score > 0) {
+ scoredCandidates.push({
+ candidate: candidate,
+ score: score
+ });
+ if (!--spotLeft) {
+ break;
+ }
+ }
+ }
+ } else {
+ queryHasSlashes = prepQuery.depth > 0;
+ coreQuery = prepQuery.core;
+ for (_j = 0, _len1 = candidates.length; _j < _len1; _j++) {
+ candidate = candidates[_j];
+ string = key != null ? candidate[key] : candidate;
+ if (!string) {
+ continue;
+ }
+ score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
+ if (!queryHasSlashes) {
+ score = legacy_scorer.basenameScore(string, coreQuery, score);
+ }
+ if (score > 0) {
+ scoredCandidates.push({
+ candidate: candidate,
+ score: score
+ });
+ }
+ }
+ }
+ scoredCandidates.sort(sortCandidates);
+ candidates = scoredCandidates.map(pluckCandidates);
+ if (maxResults != null) {
+ candidates = candidates.slice(0, maxResults);
+ }
+ return candidates;
+ };
+
+}).call(this);
+
+},{"./legacy":4,"./scorer":6,"path":7}],3:[function(require,module,exports){
+(function() {
+ var PathSeparator, filter, legacy_scorer, matcher, prepQueryCache, scorer;
+
+ scorer = require('./scorer');
+
+ legacy_scorer = require('./legacy');
+
+ filter = require('./filter');
+
+ matcher = require('./matcher');
+
+ PathSeparator = require('path').sep;
+
+ prepQueryCache = null;
+
+ module.exports = {
+ filter: function(candidates, query, options) {
+ if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) {
+ return [];
+ }
+ return filter(candidates, query, options);
+ },
+ prepQuery: function(query) {
+ return scorer.prepQuery(query);
+ },
+ score: function(string, query, prepQuery, _arg) {
+ var allowErrors, coreQuery, legacy, queryHasSlashes, score, _ref;
+ _ref = _arg != null ? _arg : {}, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
+ if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) {
+ return 0;
+ }
+ if (prepQuery == null) {
+ prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
+ }
+ if (!legacy) {
+ score = scorer.score(string, query, prepQuery, !!allowErrors);
+ } else {
+ queryHasSlashes = prepQuery.depth > 0;
+ coreQuery = prepQuery.core;
+ score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
+ if (!queryHasSlashes) {
+ score = legacy_scorer.basenameScore(string, coreQuery, score);
+ }
+ }
+ return score;
+ },
+ match: function(string, query, prepQuery, _arg) {
+ var allowErrors, baseMatches, matches, query_lw, string_lw, _i, _ref, _results;
+ allowErrors = (_arg != null ? _arg : {}).allowErrors;
+ if (!string) {
+ return [];
+ }
+ if (!query) {
+ return [];
+ }
+ if (string === query) {
+ return (function() {
+ _results = [];
+ for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
+ return _results;
+ }).apply(this);
+ }
+ if (prepQuery == null) {
+ prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
+ }
+ if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
+ return [];
+ }
+ string_lw = string.toLowerCase();
+ query_lw = prepQuery.query_lw;
+ matches = matcher.match(string, string_lw, prepQuery);
+ if (matches.length === 0) {
+ return matches;
+ }
+ if (string.indexOf(PathSeparator) > -1) {
+ baseMatches = matcher.basenameMatch(string, string_lw, prepQuery);
+ matches = matcher.mergeMatches(matches, baseMatches);
+ }
+ return matches;
+ }
+ };
+
+}).call(this);
+
+},{"./filter":2,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],4:[function(require,module,exports){
+(function() {
+ var PathSeparator, queryIsLastPathSegment;
+
+ PathSeparator = require('path').sep;
+
+ exports.basenameScore = function(string, query, score) {
+ var base, depth, index, lastCharacter, segmentCount, slashCount;
+ index = string.length - 1;
+ while (string[index] === PathSeparator) {
+ index--;
+ }
+ slashCount = 0;
+ lastCharacter = index;
+ base = null;
+ while (index >= 0) {
+ if (string[index] === PathSeparator) {
+ slashCount++;
+ if (base == null) {
+ base = string.substring(index + 1, lastCharacter + 1);
+ }
+ } else if (index === 0) {
+ if (lastCharacter < string.length - 1) {
+ if (base == null) {
+ base = string.substring(0, lastCharacter + 1);
+ }
+ } else {
+ if (base == null) {
+ base = string;
+ }
+ }
+ }
+ index--;
+ }
+ if (base === string) {
+ score *= 2;
+ } else if (base) {
+ score += exports.score(base, query);
+ }
+ segmentCount = slashCount + 1;
+ depth = Math.max(1, 10 - segmentCount);
+ score *= depth * 0.01;
+ return score;
+ };
+
+ exports.score = function(string, query) {
+ var character, characterScore, indexInQuery, indexInString, lowerCaseIndex, minIndex, queryLength, queryScore, stringLength, totalCharacterScore, upperCaseIndex, _ref;
+ if (string === query) {
+ return 1;
+ }
+ if (queryIsLastPathSegment(string, query)) {
+ return 1;
+ }
+ totalCharacterScore = 0;
+ queryLength = query.length;
+ stringLength = string.length;
+ indexInQuery = 0;
+ indexInString = 0;
+ while (indexInQuery < queryLength) {
+ character = query[indexInQuery++];
+ lowerCaseIndex = string.indexOf(character.toLowerCase());
+ upperCaseIndex = string.indexOf(character.toUpperCase());
+ minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
+ if (minIndex === -1) {
+ minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
+ }
+ indexInString = minIndex;
+ if (indexInString === -1) {
+ return 0;
+ }
+ characterScore = 0.1;
+ if (string[indexInString] === character) {
+ characterScore += 0.1;
+ }
+ if (indexInString === 0 || string[indexInString - 1] === PathSeparator) {
+ characterScore += 0.8;
+ } else if ((_ref = string[indexInString - 1]) === '-' || _ref === '_' || _ref === ' ') {
+ characterScore += 0.7;
+ }
+ string = string.substring(indexInString + 1, stringLength);
+ totalCharacterScore += characterScore;
+ }
+ queryScore = totalCharacterScore / queryLength;
+ return ((queryScore * (queryLength / stringLength)) + queryScore) / 2;
+ };
+
+ queryIsLastPathSegment = function(string, query) {
+ if (string[string.length - query.length - 1] === PathSeparator) {
+ return string.lastIndexOf(query) === string.length - query.length;
+ }
+ };
+
+ exports.match = function(string, query, stringOffset) {
+ var character, indexInQuery, indexInString, lowerCaseIndex, matches, minIndex, queryLength, stringLength, upperCaseIndex, _i, _ref, _results;
+ if (stringOffset == null) {
+ stringOffset = 0;
+ }
+ if (string === query) {
+ return (function() {
+ _results = [];
+ for (var _i = stringOffset, _ref = stringOffset + string.length; stringOffset <= _ref ? _i < _ref : _i > _ref; stringOffset <= _ref ? _i++ : _i--){ _results.push(_i); }
+ return _results;
+ }).apply(this);
+ }
+ queryLength = query.length;
+ stringLength = string.length;
+ indexInQuery = 0;
+ indexInString = 0;
+ matches = [];
+ while (indexInQuery < queryLength) {
+ character = query[indexInQuery++];
+ lowerCaseIndex = string.indexOf(character.toLowerCase());
+ upperCaseIndex = string.indexOf(character.toUpperCase());
+ minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
+ if (minIndex === -1) {
+ minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
+ }
+ indexInString = minIndex;
+ if (indexInString === -1) {
+ return [];
+ }
+ matches.push(stringOffset + indexInString);
+ stringOffset += indexInString + 1;
+ string = string.substring(indexInString + 1, stringLength);
+ }
+ return matches;
+ };
+
+}).call(this);
+
+},{"path":7}],5:[function(require,module,exports){
+(function() {
+ var PathSeparator, scorer;
+
+ PathSeparator = require('path').sep;
+
+ scorer = require('./scorer');
+
+ exports.basenameMatch = function(subject, subject_lw, prepQuery) {
+ var basePos, depth, end;
+ end = subject.length - 1;
+ while (subject[end] === PathSeparator) {
+ end--;
+ }
+ basePos = subject.lastIndexOf(PathSeparator, end);
+ if (basePos === -1) {
+ return [];
+ }
+ depth = prepQuery.depth;
+ while (depth-- > 0) {
+ basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
+ if (basePos === -1) {
+ return [];
+ }
+ }
+ basePos++;
+ end++;
+ return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos);
+ };
+
+ exports.mergeMatches = function(a, b) {
+ var ai, bj, i, j, m, n, out;
+ m = a.length;
+ n = b.length;
+ if (n === 0) {
+ return a.slice();
+ }
+ if (m === 0) {
+ return b.slice();
+ }
+ i = -1;
+ j = 0;
+ bj = b[j];
+ out = [];
+ while (++i < m) {
+ ai = a[i];
+ while (bj <= ai && ++j < n) {
+ if (bj < ai) {
+ out.push(bj);
+ }
+ bj = b[j];
+ }
+ out.push(ai);
+ }
+ while (j < n) {
+ out.push(b[j++]);
+ }
+ return out;
+ };
+
+ exports.match = function(subject, subject_lw, prepQuery, offset) {
+ var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace;
+ if (offset == null) {
+ offset = 0;
+ }
+ query = prepQuery.query;
+ query_lw = prepQuery.query_lw;
+ m = subject.length;
+ n = query.length;
+ acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score;
+ score_row = new Array(n);
+ csc_row = new Array(n);
+ STOP = 0;
+ UP = 1;
+ LEFT = 2;
+ DIAGONAL = 3;
+ trace = new Array(m * n);
+ pos = -1;
+ j = -1;
+ while (++j < n) {
+ score_row[j] = 0;
+ csc_row[j] = 0;
+ }
+ i = -1;
+ while (++i < m) {
+ score = 0;
+ score_up = 0;
+ csc_diag = 0;
+ si_lw = subject_lw[i];
+ j = -1;
+ while (++j < n) {
+ csc_score = 0;
+ align = 0;
+ score_diag = score_up;
+ if (query_lw[j] === si_lw) {
+ start = scorer.isWordStart(i, subject, subject_lw);
+ csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
+ align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score);
+ }
+ score_up = score_row[j];
+ csc_diag = csc_row[j];
+ if (score > score_up) {
+ move = LEFT;
+ } else {
+ score = score_up;
+ move = UP;
+ }
+ if (align > score) {
+ score = align;
+ move = DIAGONAL;
+ } else {
+ csc_score = 0;
+ }
+ score_row[j] = score;
+ csc_row[j] = csc_score;
+ trace[++pos] = score > 0 ? move : STOP;
+ }
+ }
+ i = m - 1;
+ j = n - 1;
+ pos = i * n + j;
+ backtrack = true;
+ matches = [];
+ while (backtrack && i >= 0 && j >= 0) {
+ switch (trace[pos]) {
+ case UP:
+ i--;
+ pos -= n;
+ break;
+ case LEFT:
+ j--;
+ pos--;
+ break;
+ case DIAGONAL:
+ matches.push(i + offset);
+ j--;
+ i--;
+ pos -= n + 1;
+ break;
+ default:
+ backtrack = false;
+ }
+ }
+ matches.reverse();
+ return matches;
+ };
+
+}).call(this);
+
+},{"./scorer":6,"path":7}],6:[function(require,module,exports){
+(function() {
+ var AcronymResult, PathSeparator, Query, basenameScore, coreChars, countDir, doScore, emptyAcronymResult, file_coeff, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm;
+
+ PathSeparator = require('path').sep;
+
+ wm = 150;
+
+ pos_bonus = 20;
+
+ tau_depth = 13;
+
+ tau_size = 85;
+
+ file_coeff = 1.2;
+
+ miss_coeff = 0.75;
+
+ opt_char_re = /[ _\-:\/\\]/g;
+
+ exports.coreChars = coreChars = function(query) {
+ return query.replace(opt_char_re, '');
+ };
+
+ exports.score = function(string, query, prepQuery, allowErrors) {
+ var score, string_lw;
+ if (prepQuery == null) {
+ prepQuery = new Query(query);
+ }
+ if (allowErrors == null) {
+ allowErrors = false;
+ }
+ if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
+ return 0;
+ }
+ string_lw = string.toLowerCase();
+ score = doScore(string, string_lw, prepQuery);
+ return Math.ceil(basenameScore(string, string_lw, prepQuery, score));
+ };
+
+ Query = (function() {
+ function Query(query) {
+ if (!(query != null ? query.length : void 0)) {
+ return null;
+ }
+ this.query = query;
+ this.query_lw = query.toLowerCase();
+ this.core = coreChars(query);
+ this.core_lw = this.core.toLowerCase();
+ this.core_up = truncatedUpperCase(this.core);
+ this.depth = countDir(query, query.length);
+ }
+
+ return Query;
+
+ })();
+
+ exports.prepQuery = function(query) {
+ return new Query(query);
+ };
+
+ exports.isMatch = isMatch = function(subject, query_lw, query_up) {
+ var i, j, m, n, qj_lw, qj_up, si;
+ m = subject.length;
+ n = query_lw.length;
+ if (!m || n > m) {
+ return false;
+ }
+ i = -1;
+ j = -1;
+ while (++j < n) {
+ qj_lw = query_lw[j];
+ qj_up = query_up[j];
+ while (++i < m) {
+ si = subject[i];
+ if (si === qj_lw || si === qj_up) {
+ break;
+ }
+ }
+ if (i === m) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ doScore = function(subject, subject_lw, prepQuery) {
+ var acro, acro_score, align, csc_diag, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz;
+ query = prepQuery.query;
+ query_lw = prepQuery.query_lw;
+ m = subject.length;
+ n = query.length;
+ acro = scoreAcronyms(subject, subject_lw, query, query_lw);
+ acro_score = acro.score;
+ if (acro.count === n) {
+ return scoreExact(n, m, acro_score, acro.pos);
+ }
+ pos = subject_lw.indexOf(query_lw);
+ if (pos > -1) {
+ return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m);
+ }
+ score_row = new Array(n);
+ csc_row = new Array(n);
+ sz = scoreSize(n, m);
+ miss_budget = Math.ceil(miss_coeff * n) + 5;
+ miss_left = miss_budget;
+ j = -1;
+ while (++j < n) {
+ score_row[j] = 0;
+ csc_row[j] = 0;
+ }
+ i = subject_lw.indexOf(query_lw[0]);
+ if (i > -1) {
+ i--;
+ }
+ mm = subject_lw.lastIndexOf(query_lw[n - 1], m);
+ if (mm > i) {
+ m = mm + 1;
+ }
+ while (++i < m) {
+ score = 0;
+ score_diag = 0;
+ csc_diag = 0;
+ si_lw = subject_lw[i];
+ record_miss = true;
+ j = -1;
+ while (++j < n) {
+ score_up = score_row[j];
+ if (score_up > score) {
+ score = score_up;
+ }
+ csc_score = 0;
+ if (query_lw[j] === si_lw) {
+ start = isWordStart(i, subject, subject_lw);
+ csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
+ align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score);
+ if (align > score) {
+ score = align;
+ miss_left = miss_budget;
+ } else {
+ if (record_miss && --miss_left <= 0) {
+ return score_row[n - 1] * sz;
+ }
+ record_miss = false;
+ }
+ }
+ score_diag = score_up;
+ csc_diag = csc_row[j];
+ csc_row[j] = csc_score;
+ score_row[j] = score;
+ }
+ }
+ return score * sz;
+ };
+
+ exports.isWordStart = isWordStart = function(pos, subject, subject_lw) {
+ var curr_s, prev_s;
+ if (pos === 0) {
+ return true;
+ }
+ curr_s = subject[pos];
+ prev_s = subject[pos - 1];
+ return isSeparator(curr_s) || isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]);
+ };
+
+ exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) {
+ var curr_s, next_s;
+ if (pos === len - 1) {
+ return true;
+ }
+ curr_s = subject[pos];
+ next_s = subject[pos + 1];
+ return isSeparator(curr_s) || isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]);
+ };
+
+ isSeparator = function(c) {
+ return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\';
+ };
+
+ scorePosition = function(pos) {
+ var sc;
+ if (pos < pos_bonus) {
+ sc = pos_bonus - pos;
+ return 100 + sc * sc;
+ } else {
+ return Math.max(100 + pos_bonus - pos, 0);
+ }
+ };
+
+ scoreSize = function(n, m) {
+ return tau_size / (tau_size + Math.abs(m - n));
+ };
+
+ scoreExact = function(n, m, quality, pos) {
+ return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m);
+ };
+
+ exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) {
+ var bonus, sz;
+ sz = count;
+ bonus = 6;
+ if (sameCase === count) {
+ bonus += 2;
+ }
+ if (start) {
+ bonus += 3;
+ }
+ if (end) {
+ bonus += 1;
+ }
+ if (count === len) {
+ if (start) {
+ if (sameCase === len) {
+ sz += 2;
+ } else {
+ sz += 1;
+ }
+ }
+ if (end) {
+ bonus += 1;
+ }
+ }
+ return sameCase + sz * (sz + bonus);
+ };
+
+ exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) {
+ var posBonus;
+ posBonus = scorePosition(i);
+ if (start) {
+ return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10);
+ }
+ return posBonus + wm * csc_score;
+ };
+
+ exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, start) {
+ var k, m, mi, n, nj, sameCase, startPos, sz;
+ m = subject.length;
+ n = query.length;
+ mi = m - i;
+ nj = n - j;
+ k = mi < nj ? mi : nj;
+ startPos = i;
+ sameCase = 0;
+ sz = 0;
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ while (++sz < k && query_lw[++j] === subject_lw[++i]) {
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ }
+ if (sz === 1) {
+ return 1 + 2 * sameCase;
+ }
+ return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m));
+ };
+
+ exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) {
+ var end, i, pos2, sameCase, start;
+ start = isWordStart(pos, subject, subject_lw);
+ if (!start) {
+ pos2 = subject_lw.indexOf(query_lw, pos + 1);
+ if (pos2 > -1) {
+ start = isWordStart(pos2, subject, subject_lw);
+ if (start) {
+ pos = pos2;
+ }
+ }
+ }
+ i = -1;
+ sameCase = 0;
+ while (++i < n) {
+ if (query[pos + i] === subject[i]) {
+ sameCase++;
+ }
+ }
+ end = isWordEnd(pos + n - 1, subject, subject_lw, m);
+ return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos);
+ };
+
+ AcronymResult = (function() {
+ function AcronymResult(score, pos, count) {
+ this.score = score;
+ this.pos = pos;
+ this.count = count;
+ }
+
+ return AcronymResult;
+
+ })();
+
+ emptyAcronymResult = new AcronymResult(0, 0.1, 0);
+
+ exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) {
+ var count, i, j, m, n, pos, qj_lw, sameCase, score;
+ m = subject.length;
+ n = query.length;
+ if (!(m > 1 && n > 1)) {
+ return emptyAcronymResult;
+ }
+ count = 0;
+ pos = 0;
+ sameCase = 0;
+ i = -1;
+ j = -1;
+ while (++j < n) {
+ qj_lw = query_lw[j];
+ while (++i < m) {
+ if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) {
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ pos += i;
+ count++;
+ break;
+ }
+ }
+ if (i === m) {
+ break;
+ }
+ }
+ if (count < 2) {
+ return emptyAcronymResult;
+ }
+ score = scorePattern(count, n, sameCase, true, false);
+ return new AcronymResult(score, pos / count, count);
+ };
+
+ basenameScore = function(subject, subject_lw, prepQuery, fullPathScore) {
+ var alpha, basePathScore, basePos, depth, end;
+ if (fullPathScore === 0) {
+ return 0;
+ }
+ end = subject.length - 1;
+ while (subject[end] === PathSeparator) {
+ end--;
+ }
+ basePos = subject.lastIndexOf(PathSeparator, end);
+ if (basePos === -1) {
+ return fullPathScore;
+ }
+ depth = prepQuery.depth;
+ while (depth-- > 0) {
+ basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
+ if (basePos === -1) {
+ return fullPathScore;
+ }
+ }
+ basePos++;
+ end++;
+ basePathScore = doScore(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery);
+ alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1));
+ return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos));
+ };
+
+ exports.countDir = countDir = function(path, end) {
+ var count, i;
+ if (end < 1) {
+ return 0;
+ }
+ count = 0;
+ i = -1;
+ while (++i < end && path[i] === PathSeparator) {
+ continue;
+ }
+ while (++i < end) {
+ if (path[i] === PathSeparator) {
+ count++;
+ while (++i < end && path[i] === PathSeparator) {
+ continue;
+ }
+ }
+ }
+ return count;
+ };
+
+ truncatedUpperCase = function(str) {
+ var char, upper, _i, _len;
+ upper = "";
+ for (_i = 0, _len = str.length; _i < _len; _i++) {
+ char = str[_i];
+ upper += char.toUpperCase()[0];
+ }
+ return upper;
+ };
+
+}).call(this);
+
+},{"path":7}],7:[function(require,module,exports){
+(function (process){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// resolves . and .. elements in a path array with directory names there
+// must be no slashes, empty elements, or device names (c:\) in the array
+// (so also no leading and trailing slashes - it does not distinguish
+// relative and absolute paths)
+function normalizeArray(parts, allowAboveRoot) {
+ // if the path tries to go above the root, `up` ends up > 0
+ var up = 0;
+ for (var i = parts.length - 1; i >= 0; i--) {
+ var last = parts[i];
+ if (last === '.') {
+ parts.splice(i, 1);
+ } else if (last === '..') {
+ parts.splice(i, 1);
+ up++;
+ } else if (up) {
+ parts.splice(i, 1);
+ up--;
+ }
+ }
+
+ // if the path is allowed to go above the root, restore leading ..s
+ if (allowAboveRoot) {
+ for (; up--; up) {
+ parts.unshift('..');
+ }
+ }
+
+ return parts;
+}
+
+// Split a filename into [root, dir, basename, ext], unix version
+// 'root' is just a slash, or nothing.
+var splitPathRe =
+ /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
+var splitPath = function(filename) {
+ return splitPathRe.exec(filename).slice(1);
+};
+
+// path.resolve([from ...], to)
+// posix version
+exports.resolve = function() {
+ var resolvedPath = '',
+ resolvedAbsolute = false;
+
+ for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
+ var path = (i >= 0) ? arguments[i] : process.cwd();
+
+ // Skip empty and invalid entries
+ if (typeof path !== 'string') {
+ throw new TypeError('Arguments to path.resolve must be strings');
+ } else if (!path) {
+ continue;
+ }
+
+ resolvedPath = path + '/' + resolvedPath;
+ resolvedAbsolute = path.charAt(0) === '/';
+ }
+
+ // At this point the path should be resolved to a full absolute path, but
+ // handle relative paths to be safe (might happen when process.cwd() fails)
+
+ // Normalize the path
+ resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
+ return !!p;
+ }), !resolvedAbsolute).join('/');
+
+ return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
+};
+
+// path.normalize(path)
+// posix version
+exports.normalize = function(path) {
+ var isAbsolute = exports.isAbsolute(path),
+ trailingSlash = substr(path, -1) === '/';
+
+ // Normalize the path
+ path = normalizeArray(filter(path.split('/'), function(p) {
+ return !!p;
+ }), !isAbsolute).join('/');
+
+ if (!path && !isAbsolute) {
+ path = '.';
+ }
+ if (path && trailingSlash) {
+ path += '/';
+ }
+
+ return (isAbsolute ? '/' : '') + path;
+};
+
+// posix version
+exports.isAbsolute = function(path) {
+ return path.charAt(0) === '/';
+};
+
+// posix version
+exports.join = function() {
+ var paths = Array.prototype.slice.call(arguments, 0);
+ return exports.normalize(filter(paths, function(p, index) {
+ if (typeof p !== 'string') {
+ throw new TypeError('Arguments to path.join must be strings');
+ }
+ return p;
+ }).join('/'));
+};
+
+
+// path.relative(from, to)
+// posix version
+exports.relative = function(from, to) {
+ from = exports.resolve(from).substr(1);
+ to = exports.resolve(to).substr(1);
+
+ function trim(arr) {
+ var start = 0;
+ for (; start < arr.length; start++) {
+ if (arr[start] !== '') break;
+ }
+
+ var end = arr.length - 1;
+ for (; end >= 0; end--) {
+ if (arr[end] !== '') break;
+ }
+
+ if (start > end) return [];
+ return arr.slice(start, end - start + 1);
+ }
+
+ var fromParts = trim(from.split('/'));
+ var toParts = trim(to.split('/'));
+
+ var length = Math.min(fromParts.length, toParts.length);
+ var samePartsLength = length;
+ for (var i = 0; i < length; i++) {
+ if (fromParts[i] !== toParts[i]) {
+ samePartsLength = i;
+ break;
+ }
+ }
+
+ var outputParts = [];
+ for (var i = samePartsLength; i < fromParts.length; i++) {
+ outputParts.push('..');
+ }
+
+ outputParts = outputParts.concat(toParts.slice(samePartsLength));
+
+ return outputParts.join('/');
+};
+
+exports.sep = '/';
+exports.delimiter = ':';
+
+exports.dirname = function(path) {
+ var result = splitPath(path),
+ root = result[0],
+ dir = result[1];
+
+ if (!root && !dir) {
+ // No dirname whatsoever
+ return '.';
+ }
+
+ if (dir) {
+ // It has a dirname, strip trailing slash
+ dir = dir.substr(0, dir.length - 1);
+ }
+
+ return root + dir;
+};
+
+
+exports.basename = function(path, ext) {
+ var f = splitPath(path)[2];
+ // TODO: make this comparison case-insensitive on windows?
+ if (ext && f.substr(-1 * ext.length) === ext) {
+ f = f.substr(0, f.length - ext.length);
+ }
+ return f;
+};
+
+
+exports.extname = function(path) {
+ return splitPath(path)[3];
+};
+
+function filter (xs, f) {
+ if (xs.filter) return xs.filter(f);
+ var res = [];
+ for (var i = 0; i < xs.length; i++) {
+ if (f(xs[i], i, xs)) res.push(xs[i]);
+ }
+ return res;
+}
+
+// String.prototype.substr - negative index don't work in IE8
+var substr = 'ab'.substr(-1) === 'b'
+ ? function (str, start, len) { return str.substr(start, len) }
+ : function (str, start, len) {
+ if (start < 0) start = str.length + start;
+ return str.substr(start, len);
+ }
+;
+
+}).call(this,require('_process'))
+},{"_process":8}],8:[function(require,module,exports){
+// shim for using process in browser
+
+var process = module.exports = {};
+var queue = [];
+var draining = false;
+var currentQueue;
+var queueIndex = -1;
+
+function cleanUpNextTick() {
+ draining = false;
+ if (currentQueue.length) {
+ queue = currentQueue.concat(queue);
+ } else {
+ queueIndex = -1;
+ }
+ if (queue.length) {
+ drainQueue();
+ }
+}
+
+function drainQueue() {
+ if (draining) {
+ return;
+ }
+ var timeout = setTimeout(cleanUpNextTick);
+ draining = true;
+
+ var len = queue.length;
+ while(len) {
+ currentQueue = queue;
+ queue = [];
+ while (++queueIndex < len) {
+ if (currentQueue) {
+ currentQueue[queueIndex].run();
+ }
+ }
+ queueIndex = -1;
+ len = queue.length;
+ }
+ currentQueue = null;
+ draining = false;
+ clearTimeout(timeout);
+}
+
+process.nextTick = function (fun) {
+ var args = new Array(arguments.length - 1);
+ if (arguments.length > 1) {
+ for (var i = 1; i < arguments.length; i++) {
+ args[i - 1] = arguments[i];
+ }
+ }
+ queue.push(new Item(fun, args));
+ if (queue.length === 1 && !draining) {
+ setTimeout(drainQueue, 0);
+ }
+};
+
+// v8 likes predictible objects
+function Item(fun, array) {
+ this.fun = fun;
+ this.array = array;
+}
+Item.prototype.run = function () {
+ this.fun.apply(null, this.array);
+};
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+process.version = ''; // empty string to avoid regexp issues
+process.versions = {};
+
+function noop() {}
+
+process.on = noop;
+process.addListener = noop;
+process.once = noop;
+process.off = noop;
+process.removeListener = noop;
+process.removeAllListeners = noop;
+process.emit = noop;
+
+process.binding = function (name) {
+ throw new Error('process.binding is not supported');
+};
+
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+ throw new Error('process.chdir is not supported');
+};
+process.umask = function() { return 0; };
+
+},{}]},{},[1]);
diff --git a/vendor/assets/javascripts/g.bar-min.js b/vendor/assets/javascripts/g.bar-min.js
deleted file mode 100644
index 7620dabda74..00000000000
--- a/vendor/assets/javascripts/g.bar-min.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/*!
- * g.Raphael 0.5 - Charting library, based on Raphaël
- *
- * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
- * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
- * From: https://github.com/jhurt/g.raphael/blob/master/g.bar.js
- */
-(function(){function c(c,d,e,f,g,h,i,j){var k,l={round:"round",sharp:"sharp",soft:"soft",square:"square"};if(g&&!f||!g&&!e)return i?"":j.path();switch(h=l[h]||"square",f=Math.round(f),e=Math.round(e),c=Math.round(c),d=Math.round(d),h){case"round":if(g)m=~~(e/2),m>f?(m=f,k=["M",c-~~(e/2),d,"l",0,0,"a",~~(e/2),m,0,0,1,e,0,"l",0,0,"z"]):k=["M",c-m,d,"l",0,m-f,"a",m,m,0,1,1,e,0,"l",0,f-m,"z"];else{var m=~~(f/2);m>e?(m=e,k=["M",c+.5,d+.5-~~(f/2),"l",0,0,"a",m,~~(f/2),0,0,1,0,f,"l",0,0,"z"]):k=["M",c+.5,d+.5-m,"l",e-m,0,"a",m,m,0,1,1,0,f,"l",m-e,0,"z"]}break;case"sharp":if(g)n=~~(e/2),k=["M",c+n,d,"l",-e,0,0,-b(f-n,0),n,-a(n,f),n,a(n,f),n,"z"];else{var n=~~(f/2);k=["M",c,d+n,"l",0,-f,b(e-n,0),0,a(n,e),n,-a(n,e),n+(f>2*n),"z"]}break;case"square":k=g?["M",c+~~(e/2),d,"l",1-e,0,0,-f,e-1,0,"z"]:["M",c,d+~~(f/2),"l",0,-f,e,0,0,f,"z"];break;case"soft":g?(m=a(Math.round(e/5),f),k=["M",c-~~(e/2),d,"l",0,m-f,"a",m,m,0,0,1,m,-m,"l",e-2*m,0,"a",m,m,0,0,1,m,m,"l",0,f-m,"z"]):(m=a(e,Math.round(f/5)),k=["M",c+.5,d+.5-~~(f/2),"l",e-m,0,"a",m,m,0,0,1,m,m,"l",0,f-2*m,"a",m,m,0,0,1,-m,m,"l",m-e,0,"z"])}return i?k.join(","):j.path(k)}function d(a,b,d,e,f,g,h){h=h||{};var i=this,j=h.type||"square",k=parseFloat(h.gutter||"20%"),l=a.set(),m=a.set(),n=a.set(),o=a.set(),p=Math.max.apply(Math,g),q=[],r=0,s=h.colors||i.colors,t=g.length;if(Raphael.is(g[0],"array")){p=[],r=t,t=0;for(var u=g.length;u--;)m.push(a.set()),p.push(Math.max.apply(Math,g[u])),t=Math.max(t,g[u].length);if(h.stacked)for(var u=t;u--;){for(var v=0,w=g.length;w--;)v+=+g[w][u]||0;q.push(v)}for(var u=g.length;u--;)if(t>g[u].length)for(var w=t;w--;)g[u].push(0);p=Math.max.apply(Math,h.stacked?q:p)}p=h.to||p;var x=100*(e/(t*(100+k)+k)),y=x*k/100,z=null==h.vgutter?20:h.vgutter,A=[],B=b+y,C=(f-2*z)/p;h.stretch||(y=Math.round(y),x=Math.floor(x)),!h.stacked&&(x/=r||1);for(var u=0;t>u;u++){A=[];for(var w=0;(r||1)>w;w++){var D=Math.round((r?g[w][u]:g[u])*C),E=d+f-z-D,F=c(Math.round(B+x/2),E+D,x,D,!0,j,null,a).attr({stroke:"none",fill:s[r?w:u]});r?m[w].push(F):m.push(F),F.y=E,F.x=Math.round(B+x/2),F.w=x,F.h=D,F.value=r?g[w][u]:g[u],h.stacked?A.push(F):B+=x}if(h.stacked){var G;o.push(G=a.rect(A[0].x-A[0].w/2,d,x,f).attr(i.shim)),G.bars=a.set();for(var H=0,I=A.length;I--;)A[I].toFront();for(var I=0,J=A.length;J>I;I++){var K,F=A[I],D=(H+F.value)*C,L=c(F.x,d+f-z-.5*!!H,x,D,!0,j,1,a);G.bars.push(F),H&&F.attr({path:L}),F.h=D,F.y=d+f-z-.5*!!H-D,n.push(K=a.rect(F.x-F.w/2,F.y,x,F.value*C).attr(i.shim)),K.bar=F,K.value=F.value,H+=F.value}B+=x}B+=y}if(o.toFront(),B=b+y,!h.stacked)for(var u=0;t>u;u++){for(var w=0;(r||1)>w;w++){var K;n.push(K=a.rect(Math.round(B),d+z,x,f-z).attr(i.shim)),K.bar=r?m[w][u]:m[u],K.value=K.bar.value,B+=x}B+=y}return l.label=function(b,c){b=b||[],this.labels=a.set();var e,j=-1/0;if(h.stacked){for(var k=0;t>k;k++)for(var l=0,o=0;(r||1)>o;o++)if(l+=r?g[o][k]:g[k],o==r-1){var q=i.labelise(b[k],l,p);e=a.text(m[o][k].x,d+f-z/2,q).attr(i.txtattr).attr({fill:h.legendcolor||"#000","text-anchor":"start"}).insertBefore(n[k*(r||1)+o]);var s=e.getBBox();j>s.x-7?e.remove():(this.labels.push(e),j=s.x+s.width)}}else for(var k=0;t>k;k++)for(var o=0;(r||1)>o;o++){var q=i.labelise(r?b[o]&&b[o][k]:b[k],r?g[o][k]:g[k],p);e=a.text(m[o][k].x-x/2,c?d+f-z/2:m[o][k].y-10,q).attr(i.txtattr).attr({fill:h.legendcolor||"#000","text-anchor":"start"}).insertBefore(n[k*(r||1)+o]);var s=e.getBBox();e.translate((x-s.width)/2,1),j>s.x-7?e.remove():(this.labels.push(e),j=s.x+s.width)}return this},l.hover=function(a,b){return o.hide(),n.show(),n.mouseover(a).mouseout(b),this},l.hoverColumn=function(a,b){return n.hide(),o.show(),b=b||function(){},o.mouseover(a).mouseout(b),this},l.click=function(a){return o.hide(),n.show(),n.click(a),this},l.each=function(a){if(!Raphael.is(a,"function"))return this;for(var b=n.length;b--;)a.call(n[b]);return this},l.eachColumn=function(a){if(!Raphael.is(a,"function"))return this;for(var b=o.length;b--;)a.call(o[b]);return this},l.clickColumn=function(a){return n.hide(),o.show(),o.click(a),this},l.push(m,n,o),l.bars=m,l.covers=n,l}function e(a,b,d,e,f,g,h){h=h||{};var i=this,j=h.type||"square",k=parseFloat(h.gutter||"20%"),l=a.set(),m=a.set(),n=a.set(),o=a.set(),p=Math.max.apply(Math,g),q=[],r=0,s=h.colors||i.colors,t=g.length;if(Raphael.is(g[0],"array")){p=[],r=t,t=0;for(var u=g.length;u--;)m.push(a.set()),p.push(Math.max.apply(Math,g[u])),t=Math.max(t,g[u].length);if(h.stacked)for(var u=t;u--;){for(var v=0,w=g.length;w--;)v+=+g[w][u]||0;q.push(v)}for(var u=g.length;u--;)if(t>g[u].length)for(var w=t;w--;)g[u].push(0);p=Math.max.apply(Math,h.stacked?q:p)}p=h.to||p;var x=Math.floor(100*(f/(t*(100+k)+k))),y=Math.floor(x*k/100),z=[],A=d+y,B=(e-1)/p;!h.stacked&&(x/=r||1);for(var u=0;t>u;u++){z=[];for(var w=0;(r||1)>w;w++){var C=r?g[w][u]:g[u],D=c(b,A+x/2,Math.round(C*B),x-1,!1,j,null,a).attr({stroke:"none",fill:s[r?w:u]});r?m[w].push(D):m.push(D),D.x=b+Math.round(C*B),D.y=A+x/2,D.w=Math.round(C*B),D.h=x,D.value=+C,h.stacked?z.push(D):A+=x}if(h.stacked){var E=a.rect(b,z[0].y-z[0].h/2,e,x).attr(i.shim);o.push(E),E.bars=a.set();for(var F=0,G=z.length;G--;)z[G].toFront();for(var G=0,H=z.length;H>G;G++){var I,D=z[G],C=Math.round((F+D.value)*B),J=c(b,D.y,C,x-1,!1,j,1,a);E.bars.push(D),F&&D.attr({path:J}),D.w=C,D.x=b+C,n.push(I=a.rect(b+F*B,D.y-D.h/2,D.value*B,x).attr(i.shim)),I.bar=D,F+=D.value}A+=x}A+=y}if(o.toFront(),A=d+y,!h.stacked)for(var u=0;t>u;u++){for(var w=0;(r||1)>w;w++){var I=a.rect(b,A,e,x).attr(i.shim);n.push(I),I.bar=r?m[w][u]:m[u],I.value=I.bar.value,A+=x}A+=y}return l.label=function(c,d){c=c||[],this.labels=a.set();for(var e=0;t>e;e++)for(var f=0;r>f;f++){var o,j=i.labelise(r?c[f]&&c[f][e]:c[e],r?g[f][e]:g[e],p),k=d?m[f][e].x-x/2+3:b+5;this.labels.push(o=a.text(k,m[f][e].y,j).attr(i.txtattr).attr({fill:h.legendcolor||"#000","text-anchor":"start"}).insertBefore(n[0])),b+5>o.getBBox().x?o.attr({x:b+5,"text-anchor":"start"}):m[f][e].label=o}return this},l.hover=function(a,b){return o.hide(),n.show(),b=b||function(){},n.mouseover(a).mouseout(b),this},l.hoverColumn=function(a,b){return n.hide(),o.show(),b=b||function(){},o.mouseover(a).mouseout(b),this},l.each=function(a){if(!Raphael.is(a,"function"))return this;for(var b=n.length;b--;)a.call(n[b]);return this},l.eachColumn=function(a){if(!Raphael.is(a,"function"))return this;for(var b=o.length;b--;)a.call(o[b]);return this},l.click=function(a){return o.hide(),n.show(),n.click(a),this},l.clickColumn=function(a){return n.hide(),o.show(),o.click(a),this},l.push(m,n,o),l.bars=m,l.covers=n,l}var a=Math.min,b=Math.max,f=function(){};f.prototype=Raphael.g,e.prototype=d.prototype=new f,Raphael.fn.hbarchart=function(a,b,c,d,f,g){return new e(this,a,b,c,d,f,g)},Raphael.fn.barchart=function(a,b,c,e,f,g){return new d(this,a,b,c,e,f,g)}})(); \ No newline at end of file
diff --git a/vendor/assets/javascripts/g.bar.js b/vendor/assets/javascripts/g.bar.js
new file mode 100644
index 00000000000..166bd654d6e
--- /dev/null
+++ b/vendor/assets/javascripts/g.bar.js
@@ -0,0 +1,674 @@
+/*!
+ * g.Raphael 0.51 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+(function () {
+ var mmin = Math.min,
+ mmax = Math.max;
+
+ function finger(x, y, width, height, dir, ending, isPath, paper) {
+ var path,
+ ends = { round: 'round', sharp: 'sharp', soft: 'soft', square: 'square' };
+
+ // dir 0 for horizontal and 1 for vertical
+ if ((dir && !height) || (!dir && !width)) {
+ return isPath ? "" : paper.path();
+ }
+
+ ending = ends[ending] || "square";
+ height = Math.round(height);
+ width = Math.round(width);
+ x = Math.round(x);
+ y = Math.round(y);
+
+ switch (ending) {
+ case "round":
+ if (!dir) {
+ var r = ~~(height / 2);
+
+ if (width < r) {
+ r = width;
+ path = [
+ "M", x + .5, y + .5 - ~~(height / 2),
+ "l", 0, 0,
+ "a", r, ~~(height / 2), 0, 0, 1, 0, height,
+ "l", 0, 0,
+ "z"
+ ];
+ } else {
+ path = [
+ "M", x + .5, y + .5 - r,
+ "l", width - r, 0,
+ "a", r, r, 0, 1, 1, 0, height,
+ "l", r - width, 0,
+ "z"
+ ];
+ }
+ } else {
+ r = ~~(width / 2);
+
+ if (height < r) {
+ r = height;
+ path = [
+ "M", x - ~~(width / 2), y,
+ "l", 0, 0,
+ "a", ~~(width / 2), r, 0, 0, 1, width, 0,
+ "l", 0, 0,
+ "z"
+ ];
+ } else {
+ path = [
+ "M", x - r, y,
+ "l", 0, r - height,
+ "a", r, r, 0, 1, 1, width, 0,
+ "l", 0, height - r,
+ "z"
+ ];
+ }
+ }
+ break;
+ case "sharp":
+ if (!dir) {
+ var half = ~~(height / 2);
+
+ path = [
+ "M", x, y + half,
+ "l", 0, -height, mmax(width - half, 0), 0, mmin(half, width), half, -mmin(half, width), half + (half * 2 < height),
+ "z"
+ ];
+ } else {
+ half = ~~(width / 2);
+ path = [
+ "M", x + half, y,
+ "l", -width, 0, 0, -mmax(height - half, 0), half, -mmin(half, height), half, mmin(half, height), half,
+ "z"
+ ];
+ }
+ break;
+ case "square":
+ if (!dir) {
+ path = [
+ "M", x, y + ~~(height / 2),
+ "l", 0, -height, width, 0, 0, height,
+ "z"
+ ];
+ } else {
+ path = [
+ "M", x + ~~(width / 2), y,
+ "l", 1 - width, 0, 0, -height, width - 1, 0,
+ "z"
+ ];
+ }
+ break;
+ case "soft":
+ if (!dir) {
+ r = mmin(width, Math.round(height / 5));
+ path = [
+ "M", x + .5, y + .5 - ~~(height / 2),
+ "l", width - r, 0,
+ "a", r, r, 0, 0, 1, r, r,
+ "l", 0, height - r * 2,
+ "a", r, r, 0, 0, 1, -r, r,
+ "l", r - width, 0,
+ "z"
+ ];
+ } else {
+ r = mmin(Math.round(width / 5), height);
+ path = [
+ "M", x - ~~(width / 2), y,
+ "l", 0, r - height,
+ "a", r, r, 0, 0, 1, r, -r,
+ "l", width - 2 * r, 0,
+ "a", r, r, 0, 0, 1, r, r,
+ "l", 0, height - r,
+ "z"
+ ];
+ }
+ }
+
+ if (isPath) {
+ return path.join(",");
+ } else {
+ return paper.path(path);
+ }
+ }
+
+/*\
+ * Paper.vbarchart
+ [ method ]
+ **
+ * Creates a vertical bar chart
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the chart
+ - y (number) y coordinate of the chart
+ - width (number) width of the chart (respected by all elements in the set)
+ - height (number) height of the chart (respected by all elements in the set)
+ - values (array) values
+ - opts (object) options for the chart
+ o {
+ o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'.
+ o gutter (number)(string) default '20%' (WHAT DOES IT DO?)
+ o vgutter (number)
+ o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color.
+ o stacked (boolean) whether or not to tread values as in a stacked bar chart
+ o to
+ o stretch (boolean)
+ o }
+ **
+ = (object) path element of the popup
+ > Usage
+ | r.vbarchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {})
+ \*/
+
+ function VBarchart(paper, x, y, width, height, values, opts) {
+ opts = opts || {};
+
+ var chartinst = this,
+ type = opts.type || "square",
+ gutter = parseFloat(opts.gutter || "20%"),
+ chart = paper.set(),
+ bars = paper.set(),
+ covers = paper.set(),
+ covers2 = paper.set(),
+ total = Math.max.apply(Math, values),
+ stacktotal = [],
+ multi = 0,
+ colors = opts.colors || chartinst.colors,
+ len = values.length;
+
+ if (Raphael.is(values[0], "array")) {
+ total = [];
+ multi = len;
+ len = 0;
+
+ for (var i = values.length; i--;) {
+ bars.push(paper.set());
+ total.push(Math.max.apply(Math, values[i]));
+ len = Math.max(len, values[i].length);
+ }
+
+ if (opts.stacked) {
+ for (var i = len; i--;) {
+ var tot = 0;
+
+ for (var j = values.length; j--;) {
+ tot +=+ values[j][i] || 0;
+ }
+
+ stacktotal.push(tot);
+ }
+ }
+
+ for (var i = values.length; i--;) {
+ if (values[i].length < len) {
+ for (var j = len; j--;) {
+ values[i].push(0);
+ }
+ }
+ }
+
+ total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
+ }
+
+ total = (opts.to) || total;
+
+ var barwidth = width / (len * (100 + gutter) + gutter) * 100,
+ barhgutter = barwidth * gutter / 100,
+ barvgutter = opts.vgutter == null ? 20 : opts.vgutter,
+ stack = [],
+ X = x + barhgutter,
+ Y = (height - 2 * barvgutter) / total;
+
+ if (!opts.stretch) {
+ barhgutter = Math.round(barhgutter);
+ barwidth = Math.floor(barwidth);
+ }
+
+ !opts.stacked && (barwidth /= multi || 1);
+
+ for (var i = 0; i < len; i++) {
+ stack = [];
+
+ for (var j = 0; j < (multi || 1); j++) {
+ var h = Math.round((multi ? values[j][i] : values[i]) * Y),
+ top = y + height - barvgutter - h,
+ bar = finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type, null, paper).attr({ stroke: "none", fill: colors[multi ? j : i] });
+
+ if (multi) {
+ bars[j].push(bar);
+ } else {
+ bars.push(bar);
+ }
+
+ bar.y = top;
+ bar.x = Math.round(X + barwidth / 2);
+ bar.w = barwidth;
+ bar.h = h;
+ bar.value = multi ? values[j][i] : values[i];
+
+ if (!opts.stacked) {
+ X += barwidth;
+ } else {
+ stack.push(bar);
+ }
+ }
+
+ if (opts.stacked) {
+ var cvr;
+
+ covers2.push(cvr = paper.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr(chartinst.shim));
+ cvr.bars = paper.set();
+
+ var size = 0;
+
+ for (var s = stack.length; s--;) {
+ stack[s].toFront();
+ }
+
+ for (var s = 0, ss = stack.length; s < ss; s++) {
+ var bar = stack[s],
+ cover,
+ h = (size + bar.value) * Y,
+ path = finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1, paper);
+
+ cvr.bars.push(bar);
+ size && bar.attr({path: path});
+ bar.h = h;
+ bar.y = y + height - barvgutter - !!size * .5 - h;
+ covers.push(cover = paper.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr(chartinst.shim));
+ cover.bar = bar;
+ cover.value = bar.value;
+ size += bar.value;
+ }
+
+ X += barwidth;
+ }
+
+ X += barhgutter;
+ }
+
+ covers2.toFront();
+ X = x + barhgutter;
+
+ if (!opts.stacked) {
+ for (var i = 0; i < len; i++) {
+ for (var j = 0; j < (multi || 1); j++) {
+ var cover;
+
+ covers.push(cover = paper.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr(chartinst.shim));
+ cover.bar = multi ? bars[j][i] : bars[i];
+ cover.value = cover.bar.value;
+ X += barwidth;
+ }
+
+ X += barhgutter;
+ }
+ }
+
+ chart.label = function (labels, isBottom) {
+ labels = labels || [];
+ this.labels = paper.set();
+
+ var L, l = -Infinity;
+
+ if (opts.stacked) {
+ for (var i = 0; i < len; i++) {
+ var tot = 0;
+
+ for (var j = 0; j < (multi || 1); j++) {
+ tot += multi ? values[j][i] : values[i];
+
+ if (j == multi - 1) {
+ var label = paper.labelise(labels[i], tot, total);
+
+ L = paper.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]);
+
+ var bb = L.getBBox();
+
+ if (bb.x - 7 < l) {
+ L.remove();
+ } else {
+ this.labels.push(L);
+ l = bb.x + bb.width;
+ }
+ }
+ }
+ }
+ } else {
+ for (var i = 0; i < len; i++) {
+ for (var j = 0; j < (multi || 1); j++) {
+ var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
+
+ L = paper.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]);
+
+ var bb = L.getBBox();
+
+ if (bb.x - 7 < l) {
+ L.remove();
+ } else {
+ this.labels.push(L);
+ l = bb.x + bb.width;
+ }
+ }
+ }
+ }
+ return this;
+ };
+
+ chart.hover = function (fin, fout) {
+ covers2.hide();
+ covers.show();
+ covers.mouseover(fin).mouseout(fout);
+ return this;
+ };
+
+ chart.hoverColumn = function (fin, fout) {
+ covers.hide();
+ covers2.show();
+ fout = fout || function () {};
+ covers2.mouseover(fin).mouseout(fout);
+ return this;
+ };
+
+ chart.click = function (f) {
+ covers2.hide();
+ covers.show();
+ covers.click(f);
+ return this;
+ };
+
+ chart.each = function (f) {
+ if (!Raphael.is(f, "function")) {
+ return this;
+ }
+ for (var i = covers.length; i--;) {
+ f.call(covers[i]);
+ }
+ return this;
+ };
+
+ chart.eachColumn = function (f) {
+ if (!Raphael.is(f, "function")) {
+ return this;
+ }
+ for (var i = covers2.length; i--;) {
+ f.call(covers2[i]);
+ }
+ return this;
+ };
+
+ chart.clickColumn = function (f) {
+ covers.hide();
+ covers2.show();
+ covers2.click(f);
+ return this;
+ };
+
+ chart.push(bars, covers, covers2);
+ chart.bars = bars;
+ chart.covers = covers;
+ return chart;
+ };
+
+ //inheritance
+ var F = function() {};
+ F.prototype = Raphael.g;
+ HBarchart.prototype = VBarchart.prototype = new F; //prototype reused by hbarchart
+
+ Raphael.fn.barchart = function(x, y, width, height, values, opts) {
+ return new VBarchart(this, x, y, width, height, values, opts);
+ };
+
+/*\
+ * Paper.barchart
+ [ method ]
+ **
+ * Creates a horizontal bar chart
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the chart
+ - y (number) y coordinate of the chart
+ - width (number) width of the chart (respected by all elements in the set)
+ - height (number) height of the chart (respected by all elements in the set)
+ - values (array) values
+ - opts (object) options for the chart
+ o {
+ o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'.
+ o gutter (number)(string) default '20%' (WHAT DOES IT DO?)
+ o vgutter (number)
+ o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color.
+ o stacked (boolean) whether or not to tread values as in a stacked bar chart
+ o to
+ o stretch (boolean)
+ o }
+ **
+ = (object) path element of the popup
+ > Usage
+ | r.barchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {})
+ \*/
+
+ function HBarchart(paper, x, y, width, height, values, opts) {
+ opts = opts || {};
+
+ var chartinst = this,
+ type = opts.type || "square",
+ gutter = parseFloat(opts.gutter || "20%"),
+ chart = paper.set(),
+ bars = paper.set(),
+ covers = paper.set(),
+ covers2 = paper.set(),
+ total = Math.max.apply(Math, values),
+ stacktotal = [],
+ multi = 0,
+ colors = opts.colors || chartinst.colors,
+ len = values.length;
+
+ if (Raphael.is(values[0], "array")) {
+ total = [];
+ multi = len;
+ len = 0;
+
+ for (var i = values.length; i--;) {
+ bars.push(paper.set());
+ total.push(Math.max.apply(Math, values[i]));
+ len = Math.max(len, values[i].length);
+ }
+
+ if (opts.stacked) {
+ for (var i = len; i--;) {
+ var tot = 0;
+ for (var j = values.length; j--;) {
+ tot +=+ values[j][i] || 0;
+ }
+ stacktotal.push(tot);
+ }
+ }
+
+ for (var i = values.length; i--;) {
+ if (values[i].length < len) {
+ for (var j = len; j--;) {
+ values[i].push(0);
+ }
+ }
+ }
+
+ total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
+ }
+
+ total = (opts.to) || total;
+
+ var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100),
+ bargutter = Math.floor(barheight * gutter / 100),
+ stack = [],
+ Y = y + bargutter,
+ X = (width - 1) / total;
+
+ !opts.stacked && (barheight /= multi || 1);
+
+ for (var i = 0; i < len; i++) {
+ stack = [];
+
+ for (var j = 0; j < (multi || 1); j++) {
+ var val = multi ? values[j][i] : values[i],
+ bar = finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type, null, paper).attr({stroke: "none", fill: colors[multi ? j : i]});
+
+ if (multi) {
+ bars[j].push(bar);
+ } else {
+ bars.push(bar);
+ }
+
+ bar.x = x + Math.round(val * X);
+ bar.y = Y + barheight / 2;
+ bar.w = Math.round(val * X);
+ bar.h = barheight;
+ bar.value = +val;
+
+ if (!opts.stacked) {
+ Y += barheight;
+ } else {
+ stack.push(bar);
+ }
+ }
+
+ if (opts.stacked) {
+ var cvr = paper.rect(x, stack[0].y - stack[0].h / 2, width, barheight).attr(chartinst.shim);
+
+ covers2.push(cvr);
+ cvr.bars = paper.set();
+
+ var size = 0;
+
+ for (var s = stack.length; s--;) {
+ stack[s].toFront();
+ }
+
+ for (var s = 0, ss = stack.length; s < ss; s++) {
+ var bar = stack[s],
+ cover,
+ val = Math.round((size + bar.value) * X),
+ path = finger(x, bar.y, val, barheight - 1, false, type, 1, paper);
+
+ cvr.bars.push(bar);
+ size && bar.attr({ path: path });
+ bar.w = val;
+ bar.x = x + val;
+ covers.push(cover = paper.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr(chartinst.shim));
+ cover.bar = bar;
+ size += bar.value;
+ }
+
+ Y += barheight;
+ }
+
+ Y += bargutter;
+ }
+
+ covers2.toFront();
+ Y = y + bargutter;
+
+ if (!opts.stacked) {
+ for (var i = 0; i < len; i++) {
+ for (var j = 0; j < (multi || 1); j++) {
+ var cover = paper.rect(x, Y, width, barheight).attr(chartinst.shim);
+
+ covers.push(cover);
+ cover.bar = multi ? bars[j][i] : bars[i];
+ cover.value = cover.bar.value;
+ Y += barheight;
+ }
+
+ Y += bargutter;
+ }
+ }
+
+ chart.label = function (labels, isRight) {
+ labels = labels || [];
+ this.labels = paper.set();
+
+ for (var i = 0; i < len; i++) {
+ for (var j = 0; j < multi; j++) {
+ var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total),
+ X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5,
+ A = isRight ? "end" : "start",
+ L;
+
+ this.labels.push(L = paper.text(X, bars[i * (multi || 1) + j].y, label).attr(txtattr).attr({ "text-anchor": A }).insertBefore(covers[0]));
+
+ if (L.getBBox().x < x + 5) {
+ L.attr({x: x + 5, "text-anchor": "start"});
+ } else {
+ bars[i * (multi || 1) + j].label = L;
+ }
+ }
+ }
+
+ return this;
+ };
+
+ chart.hover = function (fin, fout) {
+ covers2.hide();
+ covers.show();
+ fout = fout || function () {};
+ covers.mouseover(fin).mouseout(fout);
+ return this;
+ };
+
+ chart.hoverColumn = function (fin, fout) {
+ covers.hide();
+ covers2.show();
+ fout = fout || function () {};
+ covers2.mouseover(fin).mouseout(fout);
+ return this;
+ };
+
+ chart.each = function (f) {
+ if (!Raphael.is(f, "function")) {
+ return this;
+ }
+ for (var i = covers.length; i--;) {
+ f.call(covers[i]);
+ }
+ return this;
+ };
+
+ chart.eachColumn = function (f) {
+ if (!Raphael.is(f, "function")) {
+ return this;
+ }
+ for (var i = covers2.length; i--;) {
+ f.call(covers2[i]);
+ }
+ return this;
+ };
+
+ chart.click = function (f) {
+ covers2.hide();
+ covers.show();
+ covers.click(f);
+ return this;
+ };
+
+ chart.clickColumn = function (f) {
+ covers.hide();
+ covers2.show();
+ covers2.click(f);
+ return this;
+ };
+
+ chart.push(bars, covers, covers2);
+ chart.bars = bars;
+ chart.covers = covers;
+ return chart;
+ };
+
+ Raphael.fn.hbarchart = function(x, y, width, height, values, opts) {
+ return new HBarchart(this, x, y, width, height, values, opts);
+ };
+
+})();
diff --git a/vendor/assets/javascripts/g.raphael-min.js b/vendor/assets/javascripts/g.raphael-min.js
deleted file mode 100644
index f8b381c623b..00000000000
--- a/vendor/assets/javascripts/g.raphael-min.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * g.Raphael 0.51 - Charting library, based on Raphaël
- *
- * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com)
- * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
- */
-Raphael.el.popup=function(a,b,c,d){var f,g,h,i,j,e=this.paper||this[0].paper;if(e){switch(this.type){case"text":case"circle":case"ellipse":h=!0;break;default:h=!1}a=null==a?"up":a,b=b||5,f=this.getBBox(),c="number"==typeof c?c:h?f.x+f.width/2:f.x,d="number"==typeof d?d:h?f.y+f.height/2:f.y,i=Math.max(f.width/2-b,0),j=Math.max(f.height/2-b,0),this.translate(c-f.x-(h?f.width/2:0),d-f.y-(h?f.height/2:0)),f=this.getBBox();var k={up:["M",c,d,"l",-b,-b,-i,0,"a",b,b,0,0,1,-b,-b,"l",0,-f.height,"a",b,b,0,0,1,b,-b,"l",2*b+2*i,0,"a",b,b,0,0,1,b,b,"l",0,f.height,"a",b,b,0,0,1,-b,b,"l",-i,0,"z"].join(","),down:["M",c,d,"l",b,b,i,0,"a",b,b,0,0,1,b,b,"l",0,f.height,"a",b,b,0,0,1,-b,b,"l",-(2*b+2*i),0,"a",b,b,0,0,1,-b,-b,"l",0,-f.height,"a",b,b,0,0,1,b,-b,"l",i,0,"z"].join(","),left:["M",c,d,"l",-b,b,0,j,"a",b,b,0,0,1,-b,b,"l",-f.width,0,"a",b,b,0,0,1,-b,-b,"l",0,-(2*b+2*j),"a",b,b,0,0,1,b,-b,"l",f.width,0,"a",b,b,0,0,1,b,b,"l",0,j,"z"].join(","),right:["M",c,d,"l",b,-b,0,-j,"a",b,b,0,0,1,b,-b,"l",f.width,0,"a",b,b,0,0,1,b,b,"l",0,2*b+2*j,"a",b,b,0,0,1,-b,b,"l",-f.width,0,"a",b,b,0,0,1,-b,-b,"l",0,-j,"z"].join(",")};return g={up:{x:-!h*(f.width/2),y:2*-b-(h?f.height/2:f.height)},down:{x:-!h*(f.width/2),y:2*b+(h?f.height/2:f.height)},left:{x:2*-b-(h?f.width/2:f.width),y:-!h*(f.height/2)},right:{x:2*b+(h?f.width/2:f.width),y:-!h*(f.height/2)}}[a],this.translate(g.x,g.y),e.path(k[a]).attr({fill:"#000",stroke:"none"}).insertBefore(this.node?this:this[0])}},Raphael.el.tag=function(a,b,c,d){var e=3,f=this.paper||this[0].paper;if(f){var i,j,k,g=f.path().attr({fill:"#000",stroke:"#000"}),h=this.getBBox();switch(this.type){case"text":case"circle":case"ellipse":k=!0;break;default:k=!1}return a=a||0,c="number"==typeof c?c:k?h.x+h.width/2:h.x,d="number"==typeof d?d:k?h.y+h.height/2:h.y,b=null==b?5:b,j=.5522*b,h.height>=2*b?g.attr({path:["M",c,d+b,"a",b,b,0,1,1,0,2*-b,b,b,0,1,1,0,2*b,"m",0,2*-b-e,"a",b+e,b+e,0,1,0,0,2*(b+e),"L",c+b+e,d+h.height/2+e,"l",h.width+2*e,0,0,-h.height-2*e,-h.width-2*e,0,"L",c,d-b-e].join(",")}):(i=Math.sqrt(Math.pow(b+e,2)-Math.pow(h.height/2+e,2)),g.attr({path:["M",c,d+b,"c",-j,0,-b,j-b,-b,-b,0,-j,b-j,-b,b,-b,j,0,b,b-j,b,b,0,j,j-b,b,-b,b,"M",c+i,d-h.height/2-e,"a",b+e,b+e,0,1,0,0,h.height+2*e,"l",b+e-i+h.width+2*e,0,0,-h.height-2*e,"L",c+i,d-h.height/2-e].join(",")})),a=360-a,g.rotate(a,c,d),this.attrs?(this.attr(this.attrs.x?"x":"cx",c+b+e+(k?h.width/2:"text"==this.type?h.width:0)).attr("y",k?d:d-h.height/2),this.rotate(a,c,d),a>90&&270>a&&this.attr(this.attrs.x?"x":"cx",c-b-e-(k?h.width/2:h.width)).rotate(180,c,d)):a>90&&270>a?(this.translate(c-h.x-h.width-b-e,d-h.y-h.height/2),this.rotate(a-180,h.x+h.width+b+e,h.y+h.height/2)):(this.translate(c-h.x+b+e,d-h.y-h.height/2),this.rotate(a,h.x-b-e,h.y+h.height/2)),g.insertBefore(this.node?this:this[0])}},Raphael.el.drop=function(a,b,c){var f,g,h,i,j,d=this.getBBox(),e=this.paper||this[0].paper;if(e){switch(this.type){case"text":case"circle":case"ellipse":f=!0;break;default:f=!1}return a=a||0,b="number"==typeof b?b:f?d.x+d.width/2:d.x,c="number"==typeof c?c:f?d.y+d.height/2:d.y,g=Math.max(d.width,d.height)+Math.min(d.width,d.height),h=e.path(["M",b,c,"l",g,0,"A",.4*g,.4*g,0,1,0,b+.7*g,c-.7*g,"z"]).attr({fill:"#000",stroke:"none"}).rotate(22.5-a,b,c),a=(a+90)*Math.PI/180,i=b+g*Math.sin(a)-(f?0:d.width/2),j=c+g*Math.cos(a)-(f?0:d.height/2),this.attrs?this.attr(this.attrs.x?"x":"cx",i).attr(this.attrs.y?"y":"cy",j):this.translate(i-d.x,j-d.y),h.insertBefore(this.node?this:this[0])}},Raphael.el.flag=function(a,b,c){var d=3,e=this.paper||this[0].paper;if(e){var i,f=e.path().attr({fill:"#000",stroke:"#000"}),g=this.getBBox(),h=g.height/2;switch(this.type){case"text":case"circle":case"ellipse":i=!0;break;default:i=!1}return a=a||0,b="number"==typeof b?b:i?g.x+g.width/2:g.x,c="number"==typeof c?c:i?g.y+g.height/2:g.y,f.attr({path:["M",b,c,"l",h+d,-h-d,g.width+2*d,0,0,g.height+2*d,-g.width-2*d,0,"z"].join(",")}),a=360-a,f.rotate(a,b,c),this.attrs?(this.attr(this.attrs.x?"x":"cx",b+h+d+(i?g.width/2:"text"==this.type?g.width:0)).attr("y",i?c:c-g.height/2),this.rotate(a,b,c),a>90&&270>a&&this.attr(this.attrs.x?"x":"cx",b-h-d-(i?g.width/2:g.width)).rotate(180,b,c)):a>90&&270>a?(this.translate(b-g.x-g.width-h-d,c-g.y-g.height/2),this.rotate(a-180,g.x+g.width+h+d,g.y+g.height/2)):(this.translate(b-g.x+h+d,c-g.y-g.height/2),this.rotate(a,g.x-h-d,g.y+g.height/2)),f.insertBefore(this.node?this:this[0])}},Raphael.el.label=function(){var a=this.getBBox(),b=this.paper||this[0].paper,c=Math.min(20,a.width+10,a.height+10)/2;if(b)return b.rect(a.x-c/2,a.y-c/2,a.width+c,a.height+c,c).attr({stroke:"none",fill:"#000"}).insertBefore(this.node?this:this[0])},Raphael.el.blob=function(a,b,c){var g,h,i,d=this.getBBox(),e=Math.PI/180,f=this.paper||this[0].paper;if(f){switch(this.type){case"text":case"circle":case"ellipse":h=!0;break;default:h=!1}g=f.path().attr({fill:"#000",stroke:"none"}),a=(+a+1?a:45)+90,i=Math.min(d.height,d.width),b="number"==typeof b?b:h?d.x+d.width/2:d.x,c="number"==typeof c?c:h?d.y+d.height/2:d.y;var j=Math.max(d.width+i,25*i/12),k=Math.max(d.height+i,25*i/12),l=b+i*Math.sin((a-22.5)*e),m=c+i*Math.cos((a-22.5)*e),n=b+i*Math.sin((a+22.5)*e),o=c+i*Math.cos((a+22.5)*e),p=(n-l)/2,q=(o-m)/2,r=j/2,s=k/2,t=-Math.sqrt(Math.abs(r*r*s*s-r*r*q*q-s*s*p*p)/(r*r*q*q+s*s*p*p)),u=t*r*q/s+(n+l)/2,v=t*-s*p/r+(o+m)/2;return g.attr({x:u,y:v,path:["M",b,c,"L",n,o,"A",r,s,0,1,1,l,m,"z"].join(",")}),this.translate(u-d.x-d.width/2,v-d.y-d.height/2),g.insertBefore(this.node?this:this[0])}},Raphael.fn.label=function(a,b,c){var d=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),d.push(c.label(),c)},Raphael.fn.popup=function(a,b,c,d,e){var f=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),f.push(c.popup(d,e),c)},Raphael.fn.tag=function(a,b,c,d,e){var f=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),f.push(c.tag(d,e),c)},Raphael.fn.flag=function(a,b,c,d){var e=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),e.push(c.flag(d),c)},Raphael.fn.drop=function(a,b,c,d){var e=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),e.push(c.drop(d),c)},Raphael.fn.blob=function(a,b,c,d){var e=this.set();return c=this.text(a,b,c).attr(Raphael.g.txtattr),e.push(c.blob(d),c)},Raphael.el.lighter=function(a){a=a||2;var b=[this.attrs.fill,this.attrs.stroke];return this.fs=this.fs||[b[0],b[1]],b[0]=Raphael.rgb2hsb(Raphael.getRGB(b[0]).hex),b[1]=Raphael.rgb2hsb(Raphael.getRGB(b[1]).hex),b[0].b=Math.min(b[0].b*a,1),b[0].s=b[0].s/a,b[1].b=Math.min(b[1].b*a,1),b[1].s=b[1].s/a,this.attr({fill:"hsb("+[b[0].h,b[0].s,b[0].b]+")",stroke:"hsb("+[b[1].h,b[1].s,b[1].b]+")"}),this},Raphael.el.darker=function(a){a=a||2;var b=[this.attrs.fill,this.attrs.stroke];return this.fs=this.fs||[b[0],b[1]],b[0]=Raphael.rgb2hsb(Raphael.getRGB(b[0]).hex),b[1]=Raphael.rgb2hsb(Raphael.getRGB(b[1]).hex),b[0].s=Math.min(b[0].s*a,1),b[0].b=b[0].b/a,b[1].s=Math.min(b[1].s*a,1),b[1].b=b[1].b/a,this.attr({fill:"hsb("+[b[0].h,b[0].s,b[0].b]+")",stroke:"hsb("+[b[1].h,b[1].s,b[1].b]+")"}),this},Raphael.el.resetBrightness=function(){return this.fs&&(this.attr({fill:this.fs[0],stroke:this.fs[1]}),delete this.fs),this},function(){var a=["lighter","darker","resetBrightness"],b=["popup","tag","flag","label","drop","blob"];for(var c in b)(function(a){Raphael.st[a]=function(){return Raphael.el[a].apply(this,arguments)}})(b[c]);for(var c in a)(function(a){Raphael.st[a]=function(){for(var b=0;this.length>b;b++)this[b][a].apply(this[b],arguments);return this}})(a[c])}(),Raphael.g={shim:{stroke:"none",fill:"#000","fill-opacity":0},txtattr:{font:"12px Arial, sans-serif",fill:"#fff"},colors:function(){for(var a=[.6,.2,.05,.1333,.75,0],b=[],c=0;10>c;c++)a.length>c?b.push("hsb("+a[c]+",.75, .75)"):b.push("hsb("+a[c-a.length]+", 1, .5)");return b}(),snapEnds:function(a,b,c){function f(a){return.25>Math.abs(a-.5)?~~a+.5:Math.round(a)}var d=a,e=b;if(d==e)return{from:d,to:e,power:0};var g=(e-d)/c,h=~~g,i=h,j=0;if(h){for(;i;)j--,i=~~(g*Math.pow(10,j))/Math.pow(10,j);j++}else{if(0!=g&&isFinite(g))for(;!h;)j=j||1,h=~~(g*Math.pow(10,j))/Math.pow(10,j),j++;else j=1;j&&j--}return e=f(b*Math.pow(10,j))/Math.pow(10,j),b>e&&(e=f((b+.5)*Math.pow(10,j))/Math.pow(10,j)),d=f((a-(j>0?0:.5))*Math.pow(10,j))/Math.pow(10,j),{from:d,to:e,power:j}},axis:function(a,b,c,d,e,f,g,h,i,j,k){j=null==j?2:j,i=i||"t",f=f||10,k=arguments[arguments.length-1];var t,l="|"==i||" "==i?["M",a+.5,b,"l",0,.001]:1==g||3==g?["M",a+.5,b,"l",0,-c]:["M",a,b+.5,"l",c,0],m=this.snapEnds(d,e,f),n=m.from,o=m.to,p=m.power,q=0,r={font:"11px 'Fontin Sans', Fontin-Sans, sans-serif"},s=k.set();t=(o-n)/f;var u=n,v=p>0?p:0;if(z=c/f,1==+g||3==+g){for(var w=b,x=(g-1?1:-1)*(j+3+!!(g-1));w>=b-c;)"-"!=i&&" "!=i&&(l=l.concat(["M",a-("+"==i||"|"==i?j:2*!(g-1)*j),w+.5,"l",2*j+1,0])),s.push(k.text(a+x,w,h&&h[q++]||(Math.round(u)==u?u:+u.toFixed(v))).attr(r).attr({"text-anchor":g-1?"start":"end"})),u+=t,w-=z;Math.round(w+z-(b-c))&&("-"!=i&&" "!=i&&(l=l.concat(["M",a-("+"==i||"|"==i?j:2*!(g-1)*j),b-c+.5,"l",2*j+1,0])),s.push(k.text(a+x,b-c,h&&h[q]||(Math.round(u)==u?u:+u.toFixed(v))).attr(r).attr({"text-anchor":g-1?"start":"end"})))}else{u=n,v=(p>0)*p,x=(g?-1:1)*(j+9+!g);for(var y=a,z=c/f,A=0,B=0;a+c>=y;){"-"!=i&&" "!=i&&(l=l.concat(["M",y+.5,b-("+"==i?j:2*!!g*j),"l",0,2*j+1])),s.push(A=k.text(y,b+x,h&&h[q++]||(Math.round(u)==u?u:+u.toFixed(v))).attr(r));var C=A.getBBox();B>=C.x-5?s.pop(s.length-1).remove():B=C.x+C.width,u+=t,y+=z}Math.round(y-z-a-c)&&("-"!=i&&" "!=i&&(l=l.concat(["M",a+c+.5,b-("+"==i?j:2*!!g*j),"l",0,2*j+1])),s.push(k.text(a+c,b+x,h&&h[q]||(Math.round(u)==u?u:+u.toFixed(v))).attr(r)))}var D=k.path(l);return D.text=s,D.all=k.set([D,s]),D.remove=function(){this.text.remove(),this.constructor.prototype.remove.call(this)},D},labelise:function(a,b,c){return a?(a+"").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g,function(a,d,e){return d?(+b).toFixed(d.replace(/^#+\.?/g,"").length):e?(100*b/c).toFixed(e.replace(/^%+\.?/g,"").length)+"%":void 0}):(+b).toFixed(0)}}; \ No newline at end of file
diff --git a/vendor/assets/javascripts/g.raphael.js b/vendor/assets/javascripts/g.raphael.js
new file mode 100644
index 00000000000..27f27caf9f2
--- /dev/null
+++ b/vendor/assets/javascripts/g.raphael.js
@@ -0,0 +1,861 @@
+/*!
+ * g.Raphael 0.51 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+
+/*
+ * Tooltips on Element prototype
+ */
+/*\
+ * Element.popup
+ [ method ]
+ **
+ * Puts the context Element in a 'popup' tooltip. Can also be used on sets.
+ **
+ > Parameters
+ **
+ - dir (string) location of Element relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`.
+ - size (number) amount of bevel/padding around the Element, as well as half the width and height of the tail [default: `5`]
+ - x (number) x coordinate of the popup's tail [default: Element's `x` or `cx`]
+ - y (number) y coordinate of the popup's tail [default: Element's `y` or `cy`]
+ **
+ = (object) path element of the popup
+ \*/
+Raphael.el.popup = function (dir, size, x, y) {
+ var paper = this.paper || this[0].paper,
+ bb, xy, center, cw, ch;
+
+ if (!paper) return;
+
+ switch (this.type) {
+ case 'text':
+ case 'circle':
+ case 'ellipse': center = true; break;
+ default: center = false;
+ }
+
+ dir = dir == null ? 'up' : dir;
+ size = size || 5;
+ bb = this.getBBox();
+
+ x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
+ y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
+ cw = Math.max(bb.width / 2 - size, 0);
+ ch = Math.max(bb.height / 2 - size, 0);
+
+ this.translate(x - bb.x - (center ? bb.width / 2 : 0), y - bb.y - (center ? bb.height / 2 : 0));
+ bb = this.getBBox();
+
+ var paths = {
+ up: [
+ 'M', x, y,
+ 'l', -size, -size, -cw, 0,
+ 'a', size, size, 0, 0, 1, -size, -size,
+ 'l', 0, -bb.height,
+ 'a', size, size, 0, 0, 1, size, -size,
+ 'l', size * 2 + cw * 2, 0,
+ 'a', size, size, 0, 0, 1, size, size,
+ 'l', 0, bb.height,
+ 'a', size, size, 0, 0, 1, -size, size,
+ 'l', -cw, 0,
+ 'z'
+ ].join(','),
+ down: [
+ 'M', x, y,
+ 'l', size, size, cw, 0,
+ 'a', size, size, 0, 0, 1, size, size,
+ 'l', 0, bb.height,
+ 'a', size, size, 0, 0, 1, -size, size,
+ 'l', -(size * 2 + cw * 2), 0,
+ 'a', size, size, 0, 0, 1, -size, -size,
+ 'l', 0, -bb.height,
+ 'a', size, size, 0, 0, 1, size, -size,
+ 'l', cw, 0,
+ 'z'
+ ].join(','),
+ left: [
+ 'M', x, y,
+ 'l', -size, size, 0, ch,
+ 'a', size, size, 0, 0, 1, -size, size,
+ 'l', -bb.width, 0,
+ 'a', size, size, 0, 0, 1, -size, -size,
+ 'l', 0, -(size * 2 + ch * 2),
+ 'a', size, size, 0, 0, 1, size, -size,
+ 'l', bb.width, 0,
+ 'a', size, size, 0, 0, 1, size, size,
+ 'l', 0, ch,
+ 'z'
+ ].join(','),
+ right: [
+ 'M', x, y,
+ 'l', size, -size, 0, -ch,
+ 'a', size, size, 0, 0, 1, size, -size,
+ 'l', bb.width, 0,
+ 'a', size, size, 0, 0, 1, size, size,
+ 'l', 0, size * 2 + ch * 2,
+ 'a', size, size, 0, 0, 1, -size, size,
+ 'l', -bb.width, 0,
+ 'a', size, size, 0, 0, 1, -size, -size,
+ 'l', 0, -ch,
+ 'z'
+ ].join(',')
+ };
+
+ xy = {
+ up: { x: -!center * (bb.width / 2), y: -size * 2 - (center ? bb.height / 2 : bb.height) },
+ down: { x: -!center * (bb.width / 2), y: size * 2 + (center ? bb.height / 2 : bb.height) },
+ left: { x: -size * 2 - (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) },
+ right: { x: size * 2 + (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) }
+ }[dir];
+
+ this.translate(xy.x, xy.y);
+ return paper.path(paths[dir]).attr({ fill: "#000", stroke: "none" }).insertBefore(this.node ? this : this[0]);
+};
+
+/*\
+ * Element.tag
+ [ method ]
+ **
+ * Puts the context Element in a 'tag' tooltip. Can also be used on sets.
+ **
+ > Parameters
+ **
+ - angle (number) angle of orientation in degrees [default: `0`]
+ - r (number) radius of the loop [default: `5`]
+ - x (number) x coordinate of the center of the tag loop [default: Element's `x` or `cx`]
+ - y (number) y coordinate of the center of the tag loop [default: Element's `x` or `cx`]
+ **
+ = (object) path element of the tag
+ \*/
+Raphael.el.tag = function (angle, r, x, y) {
+ var d = 3,
+ paper = this.paper || this[0].paper;
+
+ if (!paper) return;
+
+ var p = paper.path().attr({ fill: '#000', stroke: '#000' }),
+ bb = this.getBBox(),
+ dx, R, center, tmp;
+
+ switch (this.type) {
+ case 'text':
+ case 'circle':
+ case 'ellipse': center = true; break;
+ default: center = false;
+ }
+
+ angle = angle || 0;
+ x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
+ y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
+ r = r == null ? 5 : r;
+ R = .5522 * r;
+
+ if (bb.height >= r * 2) {
+ p.attr({
+ path: [
+ "M", x, y + r,
+ "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2,
+ "m", 0, -r * 2 -d,
+ "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2,
+ "L", x + r + d, y + bb.height / 2 + d,
+ "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0,
+ "L", x, y - r - d
+ ].join(",")
+ });
+ } else {
+ dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2));
+ p.attr({
+ path: [
+ "M", x, y + r,
+ "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r,
+ "M", x + dx, y - bb.height / 2 - d,
+ "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d,
+ "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d,
+ "L", x + dx, y - bb.height / 2 - d
+ ].join(",")
+ });
+ }
+
+ angle = 360 - angle;
+ p.rotate(angle, x, y);
+
+ if (this.attrs) {
+ //elements
+ this.attr(this.attrs.x ? 'x' : 'cx', x + r + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2);
+ this.rotate(angle, x, y);
+ angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - r - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y);
+ } else {
+ //sets
+ if (angle > 90 && angle < 270) {
+ this.translate(x - bb.x - bb.width - r - d, y - bb.y - bb.height / 2);
+ this.rotate(angle - 180, bb.x + bb.width + r + d, bb.y + bb.height / 2);
+ } else {
+ this.translate(x - bb.x + r + d, y - bb.y - bb.height / 2);
+ this.rotate(angle, bb.x - r - d, bb.y + bb.height / 2);
+ }
+ }
+
+ return p.insertBefore(this.node ? this : this[0]);
+};
+
+/*\
+ * Element.drop
+ [ method ]
+ **
+ * Puts the context Element in a 'drop' tooltip. Can also be used on sets.
+ **
+ > Parameters
+ **
+ - angle (number) angle of orientation in degrees [default: `0`]
+ - x (number) x coordinate of the drop's point [default: Element's `x` or `cx`]
+ - y (number) y coordinate of the drop's point [default: Element's `x` or `cx`]
+ **
+ = (object) path element of the drop
+ \*/
+Raphael.el.drop = function (angle, x, y) {
+ var bb = this.getBBox(),
+ paper = this.paper || this[0].paper,
+ center, size, p, dx, dy;
+
+ if (!paper) return;
+
+ switch (this.type) {
+ case 'text':
+ case 'circle':
+ case 'ellipse': center = true; break;
+ default: center = false;
+ }
+
+ angle = angle || 0;
+
+ x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
+ y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
+ size = Math.max(bb.width, bb.height) + Math.min(bb.width, bb.height);
+ p = paper.path([
+ "M", x, y,
+ "l", size, 0,
+ "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7,
+ "z"
+ ]).attr({fill: "#000", stroke: "none"}).rotate(22.5 - angle, x, y);
+
+ angle = (angle + 90) * Math.PI / 180;
+ dx = (x + size * Math.sin(angle)) - (center ? 0 : bb.width / 2);
+ dy = (y + size * Math.cos(angle)) - (center ? 0 : bb.height / 2);
+
+ this.attrs ?
+ this.attr(this.attrs.x ? 'x' : 'cx', dx).attr(this.attrs.y ? 'y' : 'cy', dy) :
+ this.translate(dx - bb.x, dy - bb.y);
+
+ return p.insertBefore(this.node ? this : this[0]);
+};
+
+/*\
+ * Element.flag
+ [ method ]
+ **
+ * Puts the context Element in a 'flag' tooltip. Can also be used on sets.
+ **
+ > Parameters
+ **
+ - angle (number) angle of orientation in degrees [default: `0`]
+ - x (number) x coordinate of the flag's point [default: Element's `x` or `cx`]
+ - y (number) y coordinate of the flag's point [default: Element's `x` or `cx`]
+ **
+ = (object) path element of the flag
+ \*/
+Raphael.el.flag = function (angle, x, y) {
+ var d = 3,
+ paper = this.paper || this[0].paper;
+
+ if (!paper) return;
+
+ var p = paper.path().attr({ fill: '#000', stroke: '#000' }),
+ bb = this.getBBox(),
+ h = bb.height / 2,
+ center;
+
+ switch (this.type) {
+ case 'text':
+ case 'circle':
+ case 'ellipse': center = true; break;
+ default: center = false;
+ }
+
+ angle = angle || 0;
+ x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
+ y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2: bb.y);
+
+ p.attr({
+ path: [
+ "M", x, y,
+ "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0,
+ "z"
+ ].join(",")
+ });
+
+ angle = 360 - angle;
+ p.rotate(angle, x, y);
+
+ if (this.attrs) {
+ //elements
+ this.attr(this.attrs.x ? 'x' : 'cx', x + h + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2);
+ this.rotate(angle, x, y);
+ angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - h - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y);
+ } else {
+ //sets
+ if (angle > 90 && angle < 270) {
+ this.translate(x - bb.x - bb.width - h - d, y - bb.y - bb.height / 2);
+ this.rotate(angle - 180, bb.x + bb.width + h + d, bb.y + bb.height / 2);
+ } else {
+ this.translate(x - bb.x + h + d, y - bb.y - bb.height / 2);
+ this.rotate(angle, bb.x - h - d, bb.y + bb.height / 2);
+ }
+ }
+
+ return p.insertBefore(this.node ? this : this[0]);
+};
+
+/*\
+ * Element.label
+ [ method ]
+ **
+ * Puts the context Element in a 'label' tooltip. Can also be used on sets.
+ **
+ = (object) path element of the label.
+ \*/
+Raphael.el.label = function () {
+ var bb = this.getBBox(),
+ paper = this.paper || this[0].paper,
+ r = Math.min(20, bb.width + 10, bb.height + 10) / 2;
+
+ if (!paper) return;
+
+ return paper.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({ stroke: 'none', fill: '#000' }).insertBefore(this.node ? this : this[0]);
+};
+
+/*\
+ * Element.blob
+ [ method ]
+ **
+ * Puts the context Element in a 'blob' tooltip. Can also be used on sets.
+ **
+ > Parameters
+ **
+ - angle (number) angle of orientation in degrees [default: `0`]
+ - x (number) x coordinate of the blob's tail [default: Element's `x` or `cx`]
+ - y (number) y coordinate of the blob's tail [default: Element's `x` or `cx`]
+ **
+ = (object) path element of the blob
+ \*/
+Raphael.el.blob = function (angle, x, y) {
+ var bb = this.getBBox(),
+ rad = Math.PI / 180,
+ paper = this.paper || this[0].paper,
+ p, center, size;
+
+ if (!paper) return;
+
+ switch (this.type) {
+ case 'text':
+ case 'circle':
+ case 'ellipse': center = true; break;
+ default: center = false;
+ }
+
+ p = paper.path().attr({ fill: "#000", stroke: "none" });
+ angle = (+angle + 1 ? angle : 45) + 90;
+ size = Math.min(bb.height, bb.width);
+ x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
+ y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
+
+ var w = Math.max(bb.width + size, size * 25 / 12),
+ h = Math.max(bb.height + size, size * 25 / 12),
+ x2 = x + size * Math.sin((angle - 22.5) * rad),
+ y2 = y + size * Math.cos((angle - 22.5) * rad),
+ x1 = x + size * Math.sin((angle + 22.5) * rad),
+ y1 = y + size * Math.cos((angle + 22.5) * rad),
+ dx = (x1 - x2) / 2,
+ dy = (y1 - y2) / 2,
+ rx = w / 2,
+ ry = h / 2,
+ k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)),
+ cx = k * rx * dy / ry + (x1 + x2) / 2,
+ cy = k * -ry * dx / rx + (y1 + y2) / 2;
+
+ p.attr({
+ x: cx,
+ y: cy,
+ path: [
+ "M", x, y,
+ "L", x1, y1,
+ "A", rx, ry, 0, 1, 1, x2, y2,
+ "z"
+ ].join(",")
+ });
+
+ this.translate(cx - bb.x - bb.width / 2, cy - bb.y - bb.height / 2);
+
+ return p.insertBefore(this.node ? this : this[0]);
+};
+
+/*
+ * Tooltips on Paper prototype
+ */
+/*\
+ * Paper.label
+ [ method ]
+ **
+ * Puts the given `text` into a 'label' tooltip. The text is given a default style according to @g.txtattr. See @Element.label
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the center of the label
+ - y (number) y coordinate of the center of the label
+ - text (string) text to place inside the label
+ **
+ = (object) set containing the label path and the text element
+ > Usage
+ | paper.label(50, 50, "$9.99");
+ \*/
+Raphael.fn.label = function (x, y, text) {
+ var set = this.set();
+
+ text = this.text(x, y, text).attr(Raphael.g.txtattr);
+ return set.push(text.label(), text);
+};
+
+/*\
+ * Paper.popup
+ [ method ]
+ **
+ * Puts the given `text` into a 'popup' tooltip. The text is given a default style according to @g.txtattr. See @Element.popup
+ *
+ * Note: The `dir` parameter has changed from g.Raphael 0.4.1 to 0.5. The options `0`, `1`, `2`, and `3` has been changed to `'down'`, `'left'`, `'up'`, and `'right'` respectively.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the popup's tail
+ - y (number) y coordinate of the popup's tail
+ - text (string) text to place inside the popup
+ - dir (string) location of the text relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`.
+ - size (number) amount of padding around the Element [default: `5`]
+ **
+ = (object) set containing the popup path and the text element
+ > Usage
+ | paper.popup(50, 50, "$9.99", 'down');
+ \*/
+Raphael.fn.popup = function (x, y, text, dir, size) {
+ var set = this.set();
+
+ text = this.text(x, y, text).attr(Raphael.g.txtattr);
+ return set.push(text.popup(dir, size), text);
+};
+
+/*\
+ * Paper.tag
+ [ method ]
+ **
+ * Puts the given text into a 'tag' tooltip. The text is given a default style according to @g.txtattr. See @Element.tag
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the center of the tag loop
+ - y (number) y coordinate of the center of the tag loop
+ - text (string) text to place inside the tag
+ - angle (number) angle of orientation in degrees [default: `0`]
+ - r (number) radius of the loop [default: `5`]
+ **
+ = (object) set containing the tag path and the text element
+ > Usage
+ | paper.tag(50, 50, "$9.99", 60);
+ \*/
+Raphael.fn.tag = function (x, y, text, angle, r) {
+ var set = this.set();
+
+ text = this.text(x, y, text).attr(Raphael.g.txtattr);
+ return set.push(text.tag(angle, r), text);
+};
+
+/*\
+ * Paper.flag
+ [ method ]
+ **
+ * Puts the given `text` into a 'flag' tooltip. The text is given a default style according to @g.txtattr. See @Element.flag
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the flag's point
+ - y (number) y coordinate of the flag's point
+ - text (string) text to place inside the flag
+ - angle (number) angle of orientation in degrees [default: `0`]
+ **
+ = (object) set containing the flag path and the text element
+ > Usage
+ | paper.flag(50, 50, "$9.99", 60);
+ \*/
+Raphael.fn.flag = function (x, y, text, angle) {
+ var set = this.set();
+
+ text = this.text(x, y, text).attr(Raphael.g.txtattr);
+ return set.push(text.flag(angle), text);
+};
+
+/*\
+ * Paper.drop
+ [ method ]
+ **
+ * Puts the given text into a 'drop' tooltip. The text is given a default style according to @g.txtattr. See @Element.drop
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the drop's point
+ - y (number) y coordinate of the drop's point
+ - text (string) text to place inside the drop
+ - angle (number) angle of orientation in degrees [default: `0`]
+ **
+ = (object) set containing the drop path and the text element
+ > Usage
+ | paper.drop(50, 50, "$9.99", 60);
+ \*/
+Raphael.fn.drop = function (x, y, text, angle) {
+ var set = this.set();
+
+ text = this.text(x, y, text).attr(Raphael.g.txtattr);
+ return set.push(text.drop(angle), text);
+};
+
+/*\
+ * Paper.blob
+ [ method ]
+ **
+ * Puts the given text into a 'blob' tooltip. The text is given a default style according to @g.txtattr. See @Element.blob
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the blob's tail
+ - y (number) y coordinate of the blob's tail
+ - text (string) text to place inside the blob
+ - angle (number) angle of orientation in degrees [default: `0`]
+ **
+ = (object) set containing the blob path and the text element
+ > Usage
+ | paper.blob(50, 50, "$9.99", 60);
+ \*/
+Raphael.fn.blob = function (x, y, text, angle) {
+ var set = this.set();
+
+ text = this.text(x, y, text).attr(Raphael.g.txtattr);
+ return set.push(text.blob(angle), text);
+};
+
+/**
+ * Brightness functions on the Element prototype
+ */
+/*\
+ * Element.lighter
+ [ method ]
+ **
+ * Makes the context element lighter by increasing the brightness and reducing the saturation by a given factor. Can be called on Sets.
+ **
+ > Parameters
+ **
+ - times (number) adjustment factor [default: `2`]
+ **
+ = (object) Element
+ > Usage
+ | paper.circle(50, 50, 20).attr({
+ | fill: "#ff0000",
+ | stroke: "#fff",
+ | "stroke-width": 2
+ | }).lighter(6);
+ \*/
+Raphael.el.lighter = function (times) {
+ times = times || 2;
+
+ var fs = [this.attrs.fill, this.attrs.stroke];
+
+ this.fs = this.fs || [fs[0], fs[1]];
+
+ fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
+ fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
+ fs[0].b = Math.min(fs[0].b * times, 1);
+ fs[0].s = fs[0].s / times;
+ fs[1].b = Math.min(fs[1].b * times, 1);
+ fs[1].s = fs[1].s / times;
+
+ this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
+ return this;
+};
+
+/*\
+ * Element.darker
+ [ method ]
+ **
+ * Makes the context element darker by decreasing the brightness and increasing the saturation by a given factor. Can be called on Sets.
+ **
+ > Parameters
+ **
+ - times (number) adjustment factor [default: `2`]
+ **
+ = (object) Element
+ > Usage
+ | paper.circle(50, 50, 20).attr({
+ | fill: "#ff0000",
+ | stroke: "#fff",
+ | "stroke-width": 2
+ | }).darker(6);
+ \*/
+Raphael.el.darker = function (times) {
+ times = times || 2;
+
+ var fs = [this.attrs.fill, this.attrs.stroke];
+
+ this.fs = this.fs || [fs[0], fs[1]];
+
+ fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
+ fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
+ fs[0].s = Math.min(fs[0].s * times, 1);
+ fs[0].b = fs[0].b / times;
+ fs[1].s = Math.min(fs[1].s * times, 1);
+ fs[1].b = fs[1].b / times;
+
+ this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
+ return this;
+};
+
+/*\
+ * Element.resetBrightness
+ [ method ]
+ **
+ * Resets brightness and saturation levels to their original values. See @Element.lighter and @Element.darker. Can be called on Sets.
+ **
+ = (object) Element
+ > Usage
+ | paper.circle(50, 50, 20).attr({
+ | fill: "#ff0000",
+ | stroke: "#fff",
+ | "stroke-width": 2
+ | }).lighter(6).resetBrightness();
+ \*/
+Raphael.el.resetBrightness = function () {
+ if (this.fs) {
+ this.attr({ fill: this.fs[0], stroke: this.fs[1] });
+ delete this.fs;
+ }
+ return this;
+};
+
+//alias to set prototype
+(function () {
+ var brightness = ['lighter', 'darker', 'resetBrightness'],
+ tooltips = ['popup', 'tag', 'flag', 'label', 'drop', 'blob'];
+
+ for (var f in tooltips) (function (name) {
+ Raphael.st[name] = function () {
+ return Raphael.el[name].apply(this, arguments);
+ };
+ })(tooltips[f]);
+
+ for (var f in brightness) (function (name) {
+ Raphael.st[name] = function () {
+ for (var i = 0; i < this.length; i++) {
+ this[i][name].apply(this[i], arguments);
+ }
+
+ return this;
+ };
+ })(brightness[f]);
+})();
+
+//chart prototype for storing common functions
+Raphael.g = {
+ /*\
+ * g.shim
+ [ object ]
+ **
+ * An attribute object that charts will set on all generated shims (shims being the invisible objects that mouse events are bound to)
+ **
+ > Default value
+ | { stroke: 'none', fill: '#000', 'fill-opacity': 0 }
+ \*/
+ shim: { stroke: 'none', fill: '#000', 'fill-opacity': 0 },
+
+ /*\
+ * g.txtattr
+ [ object ]
+ **
+ * An attribute object that charts and tooltips will set on any generated text
+ **
+ > Default value
+ | { font: '12px Arial, sans-serif', fill: '#fff' }
+ \*/
+ txtattr: { font: '12px Arial, sans-serif', fill: '#fff' },
+
+ /*\
+ * g.colors
+ [ array ]
+ **
+ * An array of color values that charts will iterate through when drawing chart data values.
+ **
+ \*/
+ colors: (function () {
+ var hues = [.6, .2, .05, .1333, .75, 0],
+ colors = [];
+
+ for (var i = 0; i < 10; i++) {
+ if (i < hues.length) {
+ colors.push('hsb(' + hues[i] + ',.75, .75)');
+ } else {
+ colors.push('hsb(' + hues[i - hues.length] + ', 1, .5)');
+ }
+ }
+
+ return colors;
+ })(),
+
+ snapEnds: function(from, to, steps) {
+ var f = from,
+ t = to;
+
+ if (f == t) {
+ return {from: f, to: t, power: 0};
+ }
+
+ function round(a) {
+ return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a);
+ }
+
+ var d = (t - f) / steps,
+ r = ~~(d),
+ R = r,
+ i = 0;
+
+ if (r) {
+ while (R) {
+ i--;
+ R = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
+ }
+
+ i ++;
+ } else {
+ if(d == 0 || !isFinite(d)) {
+ i = 1;
+ } else {
+ while (!r) {
+ i = i || 1;
+ r = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
+ i++;
+ }
+ }
+
+ i && i--;
+ }
+
+ t = round(to * Math.pow(10, i)) / Math.pow(10, i);
+
+ if (t < to) {
+ t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i);
+ }
+
+ f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i);
+ return { from: f, to: t, power: i };
+ },
+
+ axis: function (x, y, length, from, to, steps, orientation, labels, type, dashsize, paper) {
+ dashsize = dashsize == null ? 2 : dashsize;
+ type = type || "t";
+ steps = steps || 10;
+ paper = arguments[arguments.length-1] //paper is always last argument
+
+ var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0],
+ ends = this.snapEnds(from, to, steps),
+ f = ends.from,
+ t = ends.to,
+ i = ends.power,
+ j = 0,
+ txtattr = { font: "11px 'Fontin Sans', Fontin-Sans, sans-serif" },
+ text = paper.set(),
+ d;
+
+ d = (t - f) / steps;
+
+ var label = f,
+ rnd = i > 0 ? i : 0;
+ dx = length / steps;
+
+ if (+orientation == 1 || +orientation == 3) {
+ var Y = y,
+ addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1));
+
+ while (Y >= y - length) {
+ type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0]));
+ text.push(paper.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" }));
+ label += d;
+ Y -= dx;
+ }
+
+ if (Math.round(Y + dx - (y - length))) {
+ type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0]));
+ text.push(paper.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" }));
+ }
+ } else {
+ label = f;
+ rnd = (i > 0) * i;
+ addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation);
+
+ var X = x,
+ dx = length / steps,
+ txt = 0,
+ prev = 0;
+
+ while (X <= x + length) {
+ type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
+ text.push(txt = paper.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr));
+
+ var bb = txt.getBBox();
+
+ if (prev >= bb.x - 5) {
+ text.pop(text.length - 1).remove();
+ } else {
+ prev = bb.x + bb.width;
+ }
+
+ label += d;
+ X += dx;
+ }
+
+ if (Math.round(X - dx - x - length)) {
+ type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
+ text.push(paper.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr));
+ }
+ }
+
+ var res = paper.path(path);
+
+ res.text = text;
+ res.all = paper.set([res, text]);
+ res.remove = function () {
+ this.text.remove();
+ this.constructor.prototype.remove.call(this);
+ };
+
+ return res;
+ },
+
+ labelise: function(label, val, total) {
+ if (label) {
+ return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) {
+ if (value) {
+ return (+val).toFixed(value.replace(/^#+\.?/g, "").length);
+ }
+ if (percent) {
+ return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%";
+ }
+ });
+ } else {
+ return (+val).toFixed(0);
+ }
+ }
+}
diff --git a/vendor/assets/javascripts/jquery.ba-resize.js b/vendor/assets/javascripts/jquery.ba-resize.js
new file mode 100644
index 00000000000..1f41d379153
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.ba-resize.js
@@ -0,0 +1,246 @@
+/*!
+ * jQuery resize event - v1.1 - 3/14/2010
+ * http://benalman.com/projects/jquery-resize-plugin/
+ *
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+
+// Script: jQuery resize event
+//
+// *Version: 1.1, Last updated: 3/14/2010*
+//
+// Project Home - http://benalman.com/projects/jquery-resize-plugin/
+// GitHub - http://github.com/cowboy/jquery-resize/
+// Source - http://github.com/cowboy/jquery-resize/raw/master/jquery.ba-resize.js
+// (Minified) - http://github.com/cowboy/jquery-resize/raw/master/jquery.ba-resize.min.js (1.0kb)
+//
+// About: License
+//
+// Copyright (c) 2010 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+//
+// About: Examples
+//
+// This working example, complete with fully commented code, illustrates a few
+// ways in which this plugin can be used.
+//
+// resize event - http://benalman.com/code/projects/jquery-resize/examples/resize/
+//
+// About: Support and Testing
+//
+// Information about what version or versions of jQuery this plugin has been
+// tested with, what browsers it has been tested in, and where the unit tests
+// reside (so you can test it yourself).
+//
+// jQuery Versions - 1.3.2, 1.4.1, 1.4.2
+// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome, Opera 9.6-10.1.
+// Unit Tests - http://benalman.com/code/projects/jquery-resize/unit/
+//
+// About: Release History
+//
+// 1.1 - (3/14/2010) Fixed a minor bug that was causing the event to trigger
+// immediately after bind in some circumstances. Also changed $.fn.data
+// to $.data to improve performance.
+// 1.0 - (2/10/2010) Initial release
+
+(function($,window,undefined){
+ '$:nomunge'; // Used by YUI compressor.
+
+ // A jQuery object containing all non-window elements to which the resize
+ // event is bound.
+ var elems = $([]),
+
+ // Extend $.resize if it already exists, otherwise create it.
+ jq_resize = $.resize = $.extend( $.resize, {} ),
+
+ timeout_id,
+
+ // Reused strings.
+ str_setTimeout = 'setTimeout',
+ str_resize = 'resize',
+ str_data = str_resize + '-special-event',
+ str_delay = 'delay',
+ str_throttle = 'throttleWindow';
+
+ // Property: jQuery.resize.delay
+ //
+ // The numeric interval (in milliseconds) at which the resize event polling
+ // loop executes. Defaults to 250.
+
+ jq_resize[ str_delay ] = 250;
+
+ // Property: jQuery.resize.throttleWindow
+ //
+ // Throttle the native window object resize event to fire no more than once
+ // every <jQuery.resize.delay> milliseconds. Defaults to true.
+ //
+ // Because the window object has its own resize event, it doesn't need to be
+ // provided by this plugin, and its execution can be left entirely up to the
+ // browser. However, since certain browsers fire the resize event continuously
+ // while others do not, enabling this will throttle the window resize event,
+ // making event behavior consistent across all elements in all browsers.
+ //
+ // While setting this property to false will disable window object resize
+ // event throttling, please note that this property must be changed before any
+ // window object resize event callbacks are bound.
+
+ jq_resize[ str_throttle ] = true;
+
+ // Event: resize event
+ //
+ // Fired when an element's width or height changes. Because browsers only
+ // provide this event for the window element, for other elements a polling
+ // loop is initialized, running every <jQuery.resize.delay> milliseconds
+ // to see if elements' dimensions have changed. You may bind with either
+ // .resize( fn ) or .bind( "resize", fn ), and unbind with .unbind( "resize" ).
+ //
+ // Usage:
+ //
+ // > jQuery('selector').bind( 'resize', function(e) {
+ // > // element's width or height has changed!
+ // > ...
+ // > });
+ //
+ // Additional Notes:
+ //
+ // * The polling loop is not created until at least one callback is actually
+ // bound to the 'resize' event, and this single polling loop is shared
+ // across all elements.
+ //
+ // Double firing issue in jQuery 1.3.2:
+ //
+ // While this plugin works in jQuery 1.3.2, if an element's event callbacks
+ // are manually triggered via .trigger( 'resize' ) or .resize() those
+ // callbacks may double-fire, due to limitations in the jQuery 1.3.2 special
+ // events system. This is not an issue when using jQuery 1.4+.
+ //
+ // > // While this works in jQuery 1.4+
+ // > $(elem).css({ width: new_w, height: new_h }).resize();
+ // >
+ // > // In jQuery 1.3.2, you need to do this:
+ // > var elem = $(elem);
+ // > elem.css({ width: new_w, height: new_h });
+ // > elem.data( 'resize-special-event', { width: elem.width(), height: elem.height() } );
+ // > elem.resize();
+
+ $.event.special[ str_resize ] = {
+
+ // Called only when the first 'resize' event callback is bound per element.
+ setup: function() {
+ // Since window has its own native 'resize' event, return false so that
+ // jQuery will bind the event using DOM methods. Since only 'window'
+ // objects have a .setTimeout method, this should be a sufficient test.
+ // Unless, of course, we're throttling the 'resize' event for window.
+ if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
+
+ var elem = $(this);
+
+ // Add this element to the list of internal elements to monitor.
+ elems = elems.add( elem );
+
+ // Initialize data store on the element.
+ $.data( this, str_data, { w: elem.width(), h: elem.height() } );
+
+ // If this is the first element added, start the polling loop.
+ if ( elems.length === 1 ) {
+ loopy();
+ }
+ },
+
+ // Called only when the last 'resize' event callback is unbound per element.
+ teardown: function() {
+ // Since window has its own native 'resize' event, return false so that
+ // jQuery will unbind the event using DOM methods. Since only 'window'
+ // objects have a .setTimeout method, this should be a sufficient test.
+ // Unless, of course, we're throttling the 'resize' event for window.
+ if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
+
+ var elem = $(this);
+
+ // Remove this element from the list of internal elements to monitor.
+ elems = elems.not( elem );
+
+ // Remove any data stored on the element.
+ elem.removeData( str_data );
+
+ // If this is the last element removed, stop the polling loop.
+ if ( !elems.length ) {
+ clearTimeout( timeout_id );
+ }
+ },
+
+ // Called every time a 'resize' event callback is bound per element (new in
+ // jQuery 1.4).
+ add: function( handleObj ) {
+ // Since window has its own native 'resize' event, return false so that
+ // jQuery doesn't modify the event object. Unless, of course, we're
+ // throttling the 'resize' event for window.
+ if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
+
+ var old_handler;
+
+ // The new_handler function is executed every time the event is triggered.
+ // This is used to update the internal element data store with the width
+ // and height when the event is triggered manually, to avoid double-firing
+ // of the event callback. See the "Double firing issue in jQuery 1.3.2"
+ // comments above for more information.
+
+ function new_handler( e, w, h ) {
+ var elem = $(this),
+ data = $.data( this, str_data );
+
+ // If called from the polling loop, w and h will be passed in as
+ // arguments. If called manually, via .trigger( 'resize' ) or .resize(),
+ // those values will need to be computed.
+ data.w = w !== undefined ? w : elem.width();
+ data.h = h !== undefined ? h : elem.height();
+
+ old_handler.apply( this, arguments );
+ };
+
+ // This may seem a little complicated, but it normalizes the special event
+ // .add method between jQuery 1.4/1.4.1 and 1.4.2+
+ if ( $.isFunction( handleObj ) ) {
+ // 1.4, 1.4.1
+ old_handler = handleObj;
+ return new_handler;
+ } else {
+ // 1.4.2+
+ old_handler = handleObj.handler;
+ handleObj.handler = new_handler;
+ }
+ }
+
+ };
+
+ function loopy() {
+
+ // Start the polling loop, asynchronously.
+ timeout_id = window[ str_setTimeout ](function(){
+
+ // Iterate over all elements to which the 'resize' event is bound.
+ elems.each(function(){
+ var elem = $(this),
+ width = elem.width(),
+ height = elem.height(),
+ data = $.data( this, str_data );
+
+ // If element size has changed since the last time, update the element
+ // data store and trigger the 'resize' event.
+ if ( width !== data.w || height !== data.h ) {
+ elem.trigger( str_resize, [ data.w = width, data.h = height ] );
+ }
+
+ });
+
+ // Loop.
+ loopy();
+
+ }, jq_resize[ str_delay ] );
+
+ };
+
+})(jQuery,this);
diff --git a/vendor/assets/javascripts/jquery.blockUI.js b/vendor/assets/javascripts/jquery.blockUI.js
deleted file mode 100644
index c8702d79b65..00000000000
--- a/vendor/assets/javascripts/jquery.blockUI.js
+++ /dev/null
@@ -1,590 +0,0 @@
-/*!
- * jQuery blockUI plugin
- * Version 2.60.0-2013.04.05
- * @requires jQuery v1.7 or later
- *
- * Examples at: http://malsup.com/jquery/block/
- * Copyright (c) 2007-2013 M. Alsup
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- *
- * Thanks to Amir-Hossein Sobhi for some excellent contributions!
- */
-
-;(function() {
-/*jshint eqeqeq:false curly:false latedef:false */
-"use strict";
-
- function setup($) {
- $.fn._fadeIn = $.fn.fadeIn;
-
- var noOp = $.noop || function() {};
-
- // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle
- // retarded userAgent strings on Vista)
- var msie = /MSIE/.test(navigator.userAgent);
- var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent);
- var mode = document.documentMode || 0;
- var setExpr = $.isFunction( document.createElement('div').style.setExpression );
-
- // global $ methods for blocking/unblocking the entire page
- $.blockUI = function(opts) { install(window, opts); };
- $.unblockUI = function(opts) { remove(window, opts); };
-
- // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl)
- $.growlUI = function(title, message, timeout, onClose) {
- var $m = $('<div class="growlUI"></div>');
- if (title) $m.append('<h1>'+title+'</h1>');
- if (message) $m.append('<h2>'+message+'</h2>');
- if (timeout === undefined) timeout = 3000;
- $.blockUI({
- message: $m, fadeIn: 700, fadeOut: 1000, centerY: false,
- timeout: timeout, showOverlay: false,
- onUnblock: onClose,
- css: $.blockUI.defaults.growlCSS
- });
- };
-
- // plugin method for blocking element content
- $.fn.block = function(opts) {
- if ( this[0] === window ) {
- $.blockUI( opts );
- return this;
- }
- var fullOpts = $.extend({}, $.blockUI.defaults, opts || {});
- this.each(function() {
- var $el = $(this);
- if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked'))
- return;
- $el.unblock({ fadeOut: 0 });
- });
-
- return this.each(function() {
- if ($.css(this,'position') == 'static') {
- this.style.position = 'relative';
- $(this).data('blockUI.static', true);
- }
- this.style.zoom = 1; // force 'hasLayout' in ie
- install(this, opts);
- });
- };
-
- // plugin method for unblocking element content
- $.fn.unblock = function(opts) {
- if ( this[0] === window ) {
- $.unblockUI( opts );
- return this;
- }
- return this.each(function() {
- remove(this, opts);
- });
- };
-
- $.blockUI.version = 2.60; // 2nd generation blocking at no extra cost!
-
- // override these in your code to change the default behavior and style
- $.blockUI.defaults = {
- // message displayed when blocking (use null for no message)
- message: '<h1>Please wait...</h1>',
-
- title: null, // title string; only used when theme == true
- draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded)
-
- theme: false, // set to true to use with jQuery UI themes
-
- // styles for the message when blocking; if you wish to disable
- // these and use an external stylesheet then do this in your code:
- // $.blockUI.defaults.css = {};
- css: {
- padding: 0,
- margin: 0,
- width: '30%',
- top: '40%',
- left: '35%',
- textAlign: 'center',
- color: '#000',
- border: '3px solid #aaa',
- backgroundColor:'#fff',
- cursor: 'wait'
- },
-
- // minimal style set used when themes are used
- themedCSS: {
- width: '30%',
- top: '40%',
- left: '35%'
- },
-
- // styles for the overlay
- overlayCSS: {
- backgroundColor: '#000',
- opacity: 0.6,
- cursor: 'wait'
- },
-
- // style to replace wait cursor before unblocking to correct issue
- // of lingering wait cursor
- cursorReset: 'default',
-
- // styles applied when using $.growlUI
- growlCSS: {
- width: '350px',
- top: '10px',
- left: '',
- right: '10px',
- border: 'none',
- padding: '5px',
- opacity: 0.6,
- cursor: 'default',
- color: '#fff',
- backgroundColor: '#000',
- '-webkit-border-radius':'10px',
- '-moz-border-radius': '10px',
- 'border-radius': '10px'
- },
-
- // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w
- // (hat tip to Jorge H. N. de Vasconcelos)
- /*jshint scripturl:true */
- iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank',
-
- // force usage of iframe in non-IE browsers (handy for blocking applets)
- forceIframe: false,
-
- // z-index for the blocking overlay
- baseZ: 1000,
-
- // set these to true to have the message automatically centered
- centerX: true, // <-- only effects element blocking (page block controlled via css above)
- centerY: true,
-
- // allow body element to be stetched in ie6; this makes blocking look better
- // on "short" pages. disable if you wish to prevent changes to the body height
- allowBodyStretch: true,
-
- // enable if you want key and mouse events to be disabled for content that is blocked
- bindEvents: true,
-
- // be default blockUI will supress tab navigation from leaving blocking content
- // (if bindEvents is true)
- constrainTabKey: true,
-
- // fadeIn time in millis; set to 0 to disable fadeIn on block
- fadeIn: 200,
-
- // fadeOut time in millis; set to 0 to disable fadeOut on unblock
- fadeOut: 400,
-
- // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock
- timeout: 0,
-
- // disable if you don't want to show the overlay
- showOverlay: true,
-
- // if true, focus will be placed in the first available input field when
- // page blocking
- focusInput: true,
-
- // elements that can receive focus
- focusableElements: ':input:enabled:visible',
-
- // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity)
- // no longer needed in 2012
- // applyPlatformOpacityRules: true,
-
- // callback method invoked when fadeIn has completed and blocking message is visible
- onBlock: null,
-
- // callback method invoked when unblocking has completed; the callback is
- // passed the element that has been unblocked (which is the window object for page
- // blocks) and the options that were passed to the unblock call:
- // onUnblock(element, options)
- onUnblock: null,
-
- // callback method invoked when the overlay area is clicked.
- // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used.
- onOverlayClick: null,
-
- // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493
- quirksmodeOffsetHack: 4,
-
- // class name of the message block
- blockMsgClass: 'blockMsg',
-
- // if it is already blocked, then ignore it (don't unblock and reblock)
- ignoreIfBlocked: false
- };
-
- // private data and functions follow...
-
- var pageBlock = null;
- var pageBlockEls = [];
-
- function install(el, opts) {
- var css, themedCSS;
- var full = (el == window);
- var msg = (opts && opts.message !== undefined ? opts.message : undefined);
- opts = $.extend({}, $.blockUI.defaults, opts || {});
-
- if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked'))
- return;
-
- opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {});
- css = $.extend({}, $.blockUI.defaults.css, opts.css || {});
- if (opts.onOverlayClick)
- opts.overlayCSS.cursor = 'pointer';
-
- themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {});
- msg = msg === undefined ? opts.message : msg;
-
- // remove the current block (if there is one)
- if (full && pageBlock)
- remove(window, {fadeOut:0});
-
- // if an existing element is being used as the blocking content then we capture
- // its current place in the DOM (and current display style) so we can restore
- // it when we unblock
- if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) {
- var node = msg.jquery ? msg[0] : msg;
- var data = {};
- $(el).data('blockUI.history', data);
- data.el = node;
- data.parent = node.parentNode;
- data.display = node.style.display;
- data.position = node.style.position;
- if (data.parent)
- data.parent.removeChild(node);
- }
-
- $(el).data('blockUI.onUnblock', opts.onUnblock);
- var z = opts.baseZ;
-
- // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform;
- // layer1 is the iframe layer which is used to supress bleed through of underlying content
- // layer2 is the overlay layer which has opacity and a wait cursor (by default)
- // layer3 is the message content that is displayed while blocking
- var lyr1, lyr2, lyr3, s;
- if (msie || opts.forceIframe)
- lyr1 = $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>');
- else
- lyr1 = $('<div class="blockUI" style="display:none"></div>');
-
- if (opts.theme)
- lyr2 = $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>');
- else
- lyr2 = $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');
-
- if (opts.theme && full) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">';
- if ( opts.title ) {
- s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>';
- }
- s += '<div class="ui-widget-content ui-dialog-content"></div>';
- s += '</div>';
- }
- else if (opts.theme) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">';
- if ( opts.title ) {
- s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>';
- }
- s += '<div class="ui-widget-content ui-dialog-content"></div>';
- s += '</div>';
- }
- else if (full) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>';
- }
- else {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>';
- }
- lyr3 = $(s);
-
- // if we have a message, style it
- if (msg) {
- if (opts.theme) {
- lyr3.css(themedCSS);
- lyr3.addClass('ui-widget-content');
- }
- else
- lyr3.css(css);
- }
-
- // style the overlay
- if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/)
- lyr2.css(opts.overlayCSS);
- lyr2.css('position', full ? 'fixed' : 'absolute');
-
- // make iframe layer transparent in IE
- if (msie || opts.forceIframe)
- lyr1.css('opacity',0.0);
-
- //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el);
- var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el);
- $.each(layers, function() {
- this.appendTo($par);
- });
-
- if (opts.theme && opts.draggable && $.fn.draggable) {
- lyr3.draggable({
- handle: '.ui-dialog-titlebar',
- cancel: 'li'
- });
- }
-
- // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling)
- var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0);
- if (ie6 || expr) {
- // give body 100% height
- if (full && opts.allowBodyStretch && $.support.boxModel)
- $('html,body').css('height','100%');
-
- // fix ie6 issue when blocked element has a border width
- if ((ie6 || !$.support.boxModel) && !full) {
- var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth');
- var fixT = t ? '(0 - '+t+')' : 0;
- var fixL = l ? '(0 - '+l+')' : 0;
- }
-
- // simulate fixed position
- $.each(layers, function(i,o) {
- var s = o[0].style;
- s.position = 'absolute';
- if (i < 2) {
- if (full)
- s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"');
- else
- s.setExpression('height','this.parentNode.offsetHeight + "px"');
- if (full)
- s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"');
- else
- s.setExpression('width','this.parentNode.offsetWidth + "px"');
- if (fixL) s.setExpression('left', fixL);
- if (fixT) s.setExpression('top', fixT);
- }
- else if (opts.centerY) {
- if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');
- s.marginTop = 0;
- }
- else if (!opts.centerY && full) {
- var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0;
- var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"';
- s.setExpression('top',expression);
- }
- });
- }
-
- // show the message
- if (msg) {
- if (opts.theme)
- lyr3.find('.ui-widget-content').append(msg);
- else
- lyr3.append(msg);
- if (msg.jquery || msg.nodeType)
- $(msg).show();
- }
-
- if ((msie || opts.forceIframe) && opts.showOverlay)
- lyr1.show(); // opacity is zero
- if (opts.fadeIn) {
- var cb = opts.onBlock ? opts.onBlock : noOp;
- var cb1 = (opts.showOverlay && !msg) ? cb : noOp;
- var cb2 = msg ? cb : noOp;
- if (opts.showOverlay)
- lyr2._fadeIn(opts.fadeIn, cb1);
- if (msg)
- lyr3._fadeIn(opts.fadeIn, cb2);
- }
- else {
- if (opts.showOverlay)
- lyr2.show();
- if (msg)
- lyr3.show();
- if (opts.onBlock)
- opts.onBlock();
- }
-
- // bind key and mouse events
- bind(1, el, opts);
-
- if (full) {
- pageBlock = lyr3[0];
- pageBlockEls = $(opts.focusableElements,pageBlock);
- if (opts.focusInput)
- setTimeout(focus, 20);
- }
- else
- center(lyr3[0], opts.centerX, opts.centerY);
-
- if (opts.timeout) {
- // auto-unblock
- var to = setTimeout(function() {
- if (full)
- $.unblockUI(opts);
- else
- $(el).unblock(opts);
- }, opts.timeout);
- $(el).data('blockUI.timeout', to);
- }
- }
-
- // remove the block
- function remove(el, opts) {
- var count;
- var full = (el == window);
- var $el = $(el);
- var data = $el.data('blockUI.history');
- var to = $el.data('blockUI.timeout');
- if (to) {
- clearTimeout(to);
- $el.removeData('blockUI.timeout');
- }
- opts = $.extend({}, $.blockUI.defaults, opts || {});
- bind(0, el, opts); // unbind events
-
- if (opts.onUnblock === null) {
- opts.onUnblock = $el.data('blockUI.onUnblock');
- $el.removeData('blockUI.onUnblock');
- }
-
- var els;
- if (full) // crazy selector to handle odd field errors in ie6/7
- els = $('body').children().filter('.blockUI').add('body > .blockUI');
- else
- els = $el.find('>.blockUI');
-
- // fix cursor issue
- if ( opts.cursorReset ) {
- if ( els.length > 1 )
- els[1].style.cursor = opts.cursorReset;
- if ( els.length > 2 )
- els[2].style.cursor = opts.cursorReset;
- }
-
- if (full)
- pageBlock = pageBlockEls = null;
-
- if (opts.fadeOut) {
- count = els.length;
- els.fadeOut(opts.fadeOut, function() {
- if ( --count === 0)
- reset(els,data,opts,el);
- });
- }
- else
- reset(els, data, opts, el);
- }
-
- // move blocking element back into the DOM where it started
- function reset(els,data,opts,el) {
- var $el = $(el);
- els.each(function(i,o) {
- // remove via DOM calls so we don't lose event handlers
- if (this.parentNode)
- this.parentNode.removeChild(this);
- });
-
- if (data && data.el) {
- data.el.style.display = data.display;
- data.el.style.position = data.position;
- if (data.parent)
- data.parent.appendChild(data.el);
- $el.removeData('blockUI.history');
- }
-
- if ($el.data('blockUI.static')) {
- $el.css('position', 'static'); // #22
- }
-
- if (typeof opts.onUnblock == 'function')
- opts.onUnblock(el,opts);
-
- // fix issue in Safari 6 where block artifacts remain until reflow
- var body = $(document.body), w = body.width(), cssW = body[0].style.width;
- body.width(w-1).width(w);
- body[0].style.width = cssW;
- }
-
- // bind/unbind the handler
- function bind(b, el, opts) {
- var full = el == window, $el = $(el);
-
- // don't bother unbinding if there is nothing to unbind
- if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked')))
- return;
-
- $el.data('blockUI.isBlocked', b);
-
- // don't bind events when overlay is not in use or if bindEvents is false
- if (!full || !opts.bindEvents || (b && !opts.showOverlay))
- return;
-
- // bind anchors and inputs for mouse and key events
- var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove';
- if (b)
- $(document).bind(events, opts, handler);
- else
- $(document).unbind(events, handler);
-
- // former impl...
- // var $e = $('a,:input');
- // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler);
- }
-
- // event handler to suppress keyboard/mouse events when blocking
- function handler(e) {
- // allow tab navigation (conditionally)
- if (e.keyCode && e.keyCode == 9) {
- if (pageBlock && e.data.constrainTabKey) {
- var els = pageBlockEls;
- var fwd = !e.shiftKey && e.target === els[els.length-1];
- var back = e.shiftKey && e.target === els[0];
- if (fwd || back) {
- setTimeout(function(){focus(back);},10);
- return false;
- }
- }
- }
- var opts = e.data;
- var target = $(e.target);
- if (target.hasClass('blockOverlay') && opts.onOverlayClick)
- opts.onOverlayClick();
-
- // allow events within the message content
- if (target.parents('div.' + opts.blockMsgClass).length > 0)
- return true;
-
- // allow events for content that is not being blocked
- return target.parents().children().filter('div.blockUI').length === 0;
- }
-
- function focus(back) {
- if (!pageBlockEls)
- return;
- var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0];
- if (e)
- e.focus();
- }
-
- function center(el, x, y) {
- var p = el.parentNode, s = el.style;
- var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth');
- var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth');
- if (x) s.left = l > 0 ? (l+'px') : '0';
- if (y) s.top = t > 0 ? (t+'px') : '0';
- }
-
- function sz(el, p) {
- return parseInt($.css(el,p),10)||0;
- }
-
- }
-
-
- /*global define:true */
- if (typeof define === 'function' && define.amd && define.amd.jQuery) {
- define(['jquery'], setup);
- } else {
- setup(jQuery);
- }
-
-})();
diff --git a/vendor/assets/javascripts/jquery.history.js b/vendor/assets/javascripts/jquery.history.js
deleted file mode 100644
index 8d4edcd210e..00000000000
--- a/vendor/assets/javascripts/jquery.history.js
+++ /dev/null
@@ -1 +0,0 @@
-window.JSON||(window.JSON={}),function(){function f(a){return a<10?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c<f;c+=1)h[c]=str(c,i)||"null";return e=h.length===0?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g,e}if(rep&&typeof rep=="object"){f=rep.length;for(c=0;c<f;c+=1)d=rep[c],typeof d=="string"&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e))}else for(d in i)Object.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e));return e=h.length===0?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g,e}}"use strict",typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(a){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(a){return this.valueOf()});var JSON=window.JSON,cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;typeof JSON.stringify!="function"&&(JSON.stringify=function(a,b,c){var d;gap="",indent="";if(typeof c=="number")for(d=0;d<c;d+=1)indent+=" ";else typeof c=="string"&&(indent=c);rep=b;if(!b||typeof b=="function"||typeof b=="object"&&typeof b.length=="number")return str("",{"":a});throw new Error("JSON.stringify")}),typeof JSON.parse!="function"&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.hasOwnProperty.call(e,c)&&(d=walk(e,c),d!==undefined?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),typeof reviver=="function"?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(),function(a,b){"use strict";var c=a.History=a.History||{},d=a.jQuery;if(typeof c.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");c.Adapter={bind:function(a,b,c){d(a).bind(b,c)},trigger:function(a,b,c){d(a).trigger(b,c)},extractEventData:function(a,c,d){var e=c&&c.originalEvent&&c.originalEvent[a]||d&&d[a]||b;return e},onDomLoad:function(a){d(a)}},typeof c.init!="undefined"&&c.init()}(window),function(a,b){"use strict";var c=a.document,d=a.setTimeout||d,e=a.clearTimeout||e,f=a.setInterval||f,g=a.History=a.History||{};if(typeof g.initHtml4!="undefined")throw new Error("History.js HTML4 Support has already been loaded...");g.initHtml4=function(){if(typeof g.initHtml4.initialized!="undefined")return!1;g.initHtml4.initialized=!0,g.enabled=!0,g.savedHashes=[],g.isLastHash=function(a){var b=g.getHashByIndex(),c;return c=a===b,c},g.saveHash=function(a){return g.isLastHash(a)?!1:(g.savedHashes.push(a),!0)},g.getHashByIndex=function(a){var b=null;return typeof a=="undefined"?b=g.savedHashes[g.savedHashes.length-1]:a<0?b=g.savedHashes[g.savedHashes.length+a]:b=g.savedHashes[a],b},g.discardedHashes={},g.discardedStates={},g.discardState=function(a,b,c){var d=g.getHashByState(a),e;return e={discardedState:a,backState:c,forwardState:b},g.discardedStates[d]=e,!0},g.discardHash=function(a,b,c){var d={discardedHash:a,backState:c,forwardState:b};return g.discardedHashes[a]=d,!0},g.discardedState=function(a){var b=g.getHashByState(a),c;return c=g.discardedStates[b]||!1,c},g.discardedHash=function(a){var b=g.discardedHashes[a]||!1;return b},g.recycleState=function(a){var b=g.getHashByState(a);return g.discardedState(a)&&delete g.discardedStates[b],!0},g.emulated.hashChange&&(g.hashChangeInit=function(){g.checkerFunction=null;var b="",d,e,h,i;return g.isInternetExplorer()?(d="historyjs-iframe",e=c.createElement("iframe"),e.setAttribute("id",d),e.style.display="none",c.body.appendChild(e),e.contentWindow.document.open(),e.contentWindow.document.close(),h="",i=!1,g.checkerFunction=function(){if(i)return!1;i=!0;var c=g.getHash()||"",d=g.unescapeHash(e.contentWindow.document.location.hash)||"";return c!==b?(b=c,d!==c&&(h=d=c,e.contentWindow.document.open(),e.contentWindow.document.close(),e.contentWindow.document.location.hash=g.escapeHash(c)),g.Adapter.trigger(a,"hashchange")):d!==h&&(h=d,g.setHash(d,!1)),i=!1,!0}):g.checkerFunction=function(){var c=g.getHash();return c!==b&&(b=c,g.Adapter.trigger(a,"hashchange")),!0},g.intervalList.push(f(g.checkerFunction,g.options.hashChangeInterval)),!0},g.Adapter.onDomLoad(g.hashChangeInit)),g.emulated.pushState&&(g.onHashChange=function(b){var d=b&&b.newURL||c.location.href,e=g.getHashByUrl(d),f=null,h=null,i=null,j;return g.isLastHash(e)?(g.busy(!1),!1):(g.doubleCheckComplete(),g.saveHash(e),e&&g.isTraditionalAnchor(e)?(g.Adapter.trigger(a,"anchorchange"),g.busy(!1),!1):(f=g.extractState(g.getFullUrl(e||c.location.href,!1),!0),g.isLastSavedState(f)?(g.busy(!1),!1):(h=g.getHashByState(f),j=g.discardedState(f),j?(g.getHashByIndex(-2)===g.getHashByState(j.forwardState)?g.back(!1):g.forward(!1),!1):(g.pushState(f.data,f.title,f.url,!1),!0))))},g.Adapter.bind(a,"hashchange",g.onHashChange),g.pushState=function(b,d,e,f){if(g.getHashByUrl(e))throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(f!==!1&&g.busy())return g.pushQueue({scope:g,callback:g.pushState,args:arguments,queue:f}),!1;g.busy(!0);var h=g.createStateObject(b,d,e),i=g.getHashByState(h),j=g.getState(!1),k=g.getHashByState(j),l=g.getHash();return g.storeState(h),g.expectedStateId=h.id,g.recycleState(h),g.setTitle(h),i===k?(g.busy(!1),!1):i!==l&&i!==g.getShortUrl(c.location.href)?(g.setHash(i,!1),!1):(g.saveState(h),g.Adapter.trigger(a,"statechange"),g.busy(!1),!0)},g.replaceState=function(a,b,c,d){if(g.getHashByUrl(c))throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(d!==!1&&g.busy())return g.pushQueue({scope:g,callback:g.replaceState,args:arguments,queue:d}),!1;g.busy(!0);var e=g.createStateObject(a,b,c),f=g.getState(!1),h=g.getStateByIndex(-2);return g.discardState(f,e,h),g.pushState(e.data,e.title,e.url,!1),!0}),g.emulated.pushState&&g.getHash()&&!g.emulated.hashChange&&g.Adapter.onDomLoad(function(){g.Adapter.trigger(a,"hashchange")})},typeof g.init!="undefined"&&g.init()}(window),function(a,b){"use strict";var c=a.console||b,d=a.document,e=a.navigator,f=a.sessionStorage||!1,g=a.setTimeout,h=a.clearTimeout,i=a.setInterval,j=a.clearInterval,k=a.JSON,l=a.alert,m=a.History=a.History||{},n=a.history;k.stringify=k.stringify||k.encode,k.parse=k.parse||k.decode;if(typeof m.init!="undefined")throw new Error("History.js Core has already been loaded...");m.init=function(){return typeof m.Adapter=="undefined"?!1:(typeof m.initCore!="undefined"&&m.initCore(),typeof m.initHtml4!="undefined"&&m.initHtml4(),!0)},m.initCore=function(){if(typeof m.initCore.initialized!="undefined")return!1;m.initCore.initialized=!0,m.options=m.options||{},m.options.hashChangeInterval=m.options.hashChangeInterval||100,m.options.safariPollInterval=m.options.safariPollInterval||500,m.options.doubleCheckInterval=m.options.doubleCheckInterval||500,m.options.storeInterval=m.options.storeInterval||1e3,m.options.busyDelay=m.options.busyDelay||250,m.options.debug=m.options.debug||!1,m.options.initialTitle=m.options.initialTitle||d.title,m.intervalList=[],m.clearAllIntervals=function(){var a,b=m.intervalList;if(typeof b!="undefined"&&b!==null){for(a=0;a<b.length;a++)j(b[a]);m.intervalList=null}},m.debug=function(){(m.options.debug||!1)&&m.log.apply(m,arguments)},m.log=function(){var a=typeof c!="undefined"&&typeof c.log!="undefined"&&typeof c.log.apply!="undefined",b=d.getElementById("log"),e,f,g,h,i;a?(h=Array.prototype.slice.call(arguments),e=h.shift(),typeof c.debug!="undefined"?c.debug.apply(c,[e,h]):c.log.apply(c,[e,h])):e="\n"+arguments[0]+"\n";for(f=1,g=arguments.length;f<g;++f){i=arguments[f];if(typeof i=="object"&&typeof k!="undefined")try{i=k.stringify(i)}catch(j){}e+="\n"+i+"\n"}return b?(b.value+=e+"\n-----\n",b.scrollTop=b.scrollHeight-b.clientHeight):a||l(e),!0},m.getInternetExplorerMajorVersion=function(){var a=m.getInternetExplorerMajorVersion.cached=typeof m.getInternetExplorerMajorVersion.cached!="undefined"?m.getInternetExplorerMajorVersion.cached:function(){var a=3,b=d.createElement("div"),c=b.getElementsByTagName("i");while((b.innerHTML="<!--[if gt IE "+ ++a+"]><i></i><![endif]-->")&&c[0]);return a>4?a:!1}();return a},m.isInternetExplorer=function(){var a=m.isInternetExplorer.cached=typeof m.isInternetExplorer.cached!="undefined"?m.isInternetExplorer.cached:Boolean(m.getInternetExplorerMajorVersion());return a},m.emulated={pushState:!Boolean(a.history&&a.history.pushState&&a.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)),hashChange:Boolean(!("onhashchange"in a||"onhashchange"in d)||m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8)},m.enabled=!m.emulated.pushState,m.bugs={setHash:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),safariPoll:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),ieDoubleCheck:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<7)},m.isEmptyObject=function(a){for(var b in a)return!1;return!0},m.cloneObject=function(a){var b,c;return a?(b=k.stringify(a),c=k.parse(b)):c={},c},m.getRootUrl=function(){var a=d.location.protocol+"//"+(d.location.hostname||d.location.host);if(d.location.port||!1)a+=":"+d.location.port;return a+="/",a},m.getBaseHref=function(){var a=d.getElementsByTagName("base"),b=null,c="";return a.length===1&&(b=a[0],c=b.href.replace(/[^\/]+$/,"")),c=c.replace(/\/+$/,""),c&&(c+="/"),c},m.getBaseUrl=function(){var a=m.getBaseHref()||m.getBasePageUrl()||m.getRootUrl();return a},m.getPageUrl=function(){var a=m.getState(!1,!1),b=(a||{}).url||d.location.href,c;return c=b.replace(/\/+$/,"").replace(/[^\/]+$/,function(a,b,c){return/\./.test(a)?a:a+"/"}),c},m.getBasePageUrl=function(){var a=d.location.href.replace(/[#\?].*/,"").replace(/[^\/]+$/,function(a,b,c){return/[^\/]$/.test(a)?"":a}).replace(/\/+$/,"")+"/";return a},m.getFullUrl=function(a,b){var c=a,d=a.substring(0,1);return b=typeof b=="undefined"?!0:b,/[a-z]+\:\/\//.test(a)||(d==="/"?c=m.getRootUrl()+a.replace(/^\/+/,""):d==="#"?c=m.getPageUrl().replace(/#.*/,"")+a:d==="?"?c=m.getPageUrl().replace(/[\?#].*/,"")+a:b?c=m.getBaseUrl()+a.replace(/^(\.\/)+/,""):c=m.getBasePageUrl()+a.replace(/^(\.\/)+/,"")),c.replace(/\#$/,"")},m.getShortUrl=function(a){var b=a,c=m.getBaseUrl(),d=m.getRootUrl();return m.emulated.pushState&&(b=b.replace(c,"")),b=b.replace(d,"/"),m.isTraditionalAnchor(b)&&(b="./"+b),b=b.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),b},m.store={},m.idToState=m.idToState||{},m.stateToId=m.stateToId||{},m.urlToId=m.urlToId||{},m.storedStates=m.storedStates||[],m.savedStates=m.savedStates||[],m.normalizeStore=function(){m.store.idToState=m.store.idToState||{},m.store.urlToId=m.store.urlToId||{},m.store.stateToId=m.store.stateToId||{}},m.getState=function(a,b){typeof a=="undefined"&&(a=!0),typeof b=="undefined"&&(b=!0);var c=m.getLastSavedState();return!c&&b&&(c=m.createStateObject()),a&&(c=m.cloneObject(c),c.url=c.cleanUrl||c.url),c},m.getIdByState=function(a){var b=m.extractId(a.url),c;if(!b){c=m.getStateString(a);if(typeof m.stateToId[c]!="undefined")b=m.stateToId[c];else if(typeof m.store.stateToId[c]!="undefined")b=m.store.stateToId[c];else{for(;;){b=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof m.idToState[b]=="undefined"&&typeof m.store.idToState[b]=="undefined")break}m.stateToId[c]=b,m.idToState[b]=a}}return b},m.normalizeState=function(a){var b,c;if(!a||typeof a!="object")a={};if(typeof a.normalized!="undefined")return a;if(!a.data||typeof a.data!="object")a.data={};b={},b.normalized=!0,b.title=a.title||"",b.url=m.getFullUrl(m.unescapeString(a.url||d.location.href)),b.hash=m.getShortUrl(b.url),b.data=m.cloneObject(a.data),b.id=m.getIdByState(b),b.cleanUrl=b.url.replace(/\??\&_suid.*/,""),b.url=b.cleanUrl,c=!m.isEmptyObject(b.data);if(b.title||c)b.hash=m.getShortUrl(b.url).replace(/\??\&_suid.*/,""),/\?/.test(b.hash)||(b.hash+="?"),b.hash+="&_suid="+b.id;return b.hashedUrl=m.getFullUrl(b.hash),(m.emulated.pushState||m.bugs.safariPoll)&&m.hasUrlDuplicate(b)&&(b.url=b.hashedUrl),b},m.createStateObject=function(a,b,c){var d={data:a,title:b,url:c};return d=m.normalizeState(d),d},m.getStateById=function(a){a=String(a);var c=m.idToState[a]||m.store.idToState[a]||b;return c},m.getStateString=function(a){var b,c,d;return b=m.normalizeState(a),c={data:b.data,title:a.title,url:a.url},d=k.stringify(c),d},m.getStateId=function(a){var b,c;return b=m.normalizeState(a),c=b.id,c},m.getHashByState=function(a){var b,c;return b=m.normalizeState(a),c=b.hash,c},m.extractId=function(a){var b,c,d;return c=/(.*)\&_suid=([0-9]+)$/.exec(a),d=c?c[1]||a:a,b=c?String(c[2]||""):"",b||!1},m.isTraditionalAnchor=function(a){var b=!/[\/\?\.]/.test(a);return b},m.extractState=function(a,b){var c=null,d,e;return b=b||!1,d=m.extractId(a),d&&(c=m.getStateById(d)),c||(e=m.getFullUrl(a),d=m.getIdByUrl(e)||!1,d&&(c=m.getStateById(d)),!c&&b&&!m.isTraditionalAnchor(a)&&(c=m.createStateObject(null,null,e))),c},m.getIdByUrl=function(a){var c=m.urlToId[a]||m.store.urlToId[a]||b;return c},m.getLastSavedState=function(){return m.savedStates[m.savedStates.length-1]||b},m.getLastStoredState=function(){return m.storedStates[m.storedStates.length-1]||b},m.hasUrlDuplicate=function(a){var b=!1,c;return c=m.extractState(a.url),b=c&&c.id!==a.id,b},m.storeState=function(a){return m.urlToId[a.url]=a.id,m.storedStates.push(m.cloneObject(a)),a},m.isLastSavedState=function(a){var b=!1,c,d,e;return m.savedStates.length&&(c=a.id,d=m.getLastSavedState(),e=d.id,b=c===e),b},m.saveState=function(a){return m.isLastSavedState(a)?!1:(m.savedStates.push(m.cloneObject(a)),!0)},m.getStateByIndex=function(a){var b=null;return typeof a=="undefined"?b=m.savedStates[m.savedStates.length-1]:a<0?b=m.savedStates[m.savedStates.length+a]:b=m.savedStates[a],b},m.getHash=function(){var a=m.unescapeHash(d.location.hash);return a},m.unescapeString=function(b){var c=b,d;for(;;){d=a.unescape(c);if(d===c)break;c=d}return c},m.unescapeHash=function(a){var b=m.normalizeHash(a);return b=m.unescapeString(b),b},m.normalizeHash=function(a){var b=a.replace(/[^#]*#/,"").replace(/#.*/,"");return b},m.setHash=function(a,b){var c,e,f;return b!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.setHash,args:arguments,queue:b}),!1):(c=m.escapeHash(a),m.busy(!0),e=m.extractState(a,!0),e&&!m.emulated.pushState?m.pushState(e.data,e.title,e.url,!1):d.location.hash!==c&&(m.bugs.setHash?(f=m.getPageUrl(),m.pushState(null,null,f+"#"+c,!1)):d.location.hash=c),m)},m.escapeHash=function(b){var c=m.normalizeHash(b);return c=a.escape(c),m.bugs.hashEscape||(c=c.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),c},m.getHashByUrl=function(a){var b=String(a).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return b=m.unescapeHash(b),b},m.setTitle=function(a){var b=a.title,c;b||(c=m.getStateByIndex(0),c&&c.url===a.url&&(b=c.title||m.options.initialTitle));try{d.getElementsByTagName("title")[0].innerHTML=b.replace("<","&lt;").replace(">","&gt;").replace(" & "," &amp; ")}catch(e){}return d.title=b,m},m.queues=[],m.busy=function(a){typeof a!="undefined"?m.busy.flag=a:typeof m.busy.flag=="undefined"&&(m.busy.flag=!1);if(!m.busy.flag){h(m.busy.timeout);var b=function(){var a,c,d;if(m.busy.flag)return;for(a=m.queues.length-1;a>=0;--a){c=m.queues[a];if(c.length===0)continue;d=c.shift(),m.fireQueueItem(d),m.busy.timeout=g(b,m.options.busyDelay)}};m.busy.timeout=g(b,m.options.busyDelay)}return m.busy.flag},m.busy.flag=!1,m.fireQueueItem=function(a){return a.callback.apply(a.scope||m,a.args||[])},m.pushQueue=function(a){return m.queues[a.queue||0]=m.queues[a.queue||0]||[],m.queues[a.queue||0].push(a),m},m.queue=function(a,b){return typeof a=="function"&&(a={callback:a}),typeof b!="undefined"&&(a.queue=b),m.busy()?m.pushQueue(a):m.fireQueueItem(a),m},m.clearQueue=function(){return m.busy.flag=!1,m.queues=[],m},m.stateChanged=!1,m.doubleChecker=!1,m.doubleCheckComplete=function(){return m.stateChanged=!0,m.doubleCheckClear(),m},m.doubleCheckClear=function(){return m.doubleChecker&&(h(m.doubleChecker),m.doubleChecker=!1),m},m.doubleCheck=function(a){return m.stateChanged=!1,m.doubleCheckClear(),m.bugs.ieDoubleCheck&&(m.doubleChecker=g(function(){return m.doubleCheckClear(),m.stateChanged||a(),!0},m.options.doubleCheckInterval)),m},m.safariStatePoll=function(){var b=m.extractState(d.location.href),c;if(!m.isLastSavedState(b))c=b;else return;return c||(c=m.createStateObject()),m.Adapter.trigger(a,"popstate"),m},m.back=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.back,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.back(!1)}),n.go(-1),!0)},m.forward=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.forward,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.forward(!1)}),n.go(1),!0)},m.go=function(a,b){var c;if(a>0)for(c=1;c<=a;++c)m.forward(b);else{if(!(a<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(c=-1;c>=a;--c)m.back(b)}return m};if(m.emulated.pushState){var o=function(){};m.pushState=m.pushState||o,m.replaceState=m.replaceState||o}else m.onPopState=function(b,c){var e=!1,f=!1,g,h;return m.doubleCheckComplete(),g=m.getHash(),g?(h=m.extractState(g||d.location.href,!0),h?m.replaceState(h.data,h.title,h.url,!1):(m.Adapter.trigger(a,"anchorchange"),m.busy(!1)),m.expectedStateId=!1,!1):(e=m.Adapter.extractEventData("state",b,c)||!1,e?f=m.getStateById(e):m.expectedStateId?f=m.getStateById(m.expectedStateId):f=m.extractState(d.location.href),f||(f=m.createStateObject(null,null,d.location.href)),m.expectedStateId=!1,m.isLastSavedState(f)?(m.busy(!1),!1):(m.storeState(f),m.saveState(f),m.setTitle(f),m.Adapter.trigger(a,"statechange"),m.busy(!1),!0))},m.Adapter.bind(a,"popstate",m.onPopState),m.pushState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.pushState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.pushState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0},m.replaceState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.replaceState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.replaceState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0};if(f){try{m.store=k.parse(f.getItem("History.store"))||{}}catch(p){m.store={}}m.normalizeStore()}else m.store={},m.normalizeStore();m.Adapter.bind(a,"beforeunload",m.clearAllIntervals),m.Adapter.bind(a,"unload",m.clearAllIntervals),m.saveState(m.storeState(m.extractState(d.location.href,!0))),f&&(m.onUnload=function(){var a,b;try{a=k.parse(f.getItem("History.store"))||{}}catch(c){a={}}a.idToState=a.idToState||{},a.urlToId=a.urlToId||{},a.stateToId=a.stateToId||{};for(b in m.idToState){if(!m.idToState.hasOwnProperty(b))continue;a.idToState[b]=m.idToState[b]}for(b in m.urlToId){if(!m.urlToId.hasOwnProperty(b))continue;a.urlToId[b]=m.urlToId[b]}for(b in m.stateToId){if(!m.stateToId.hasOwnProperty(b))continue;a.stateToId[b]=m.stateToId[b]}m.store=a,m.normalizeStore(),f.setItem("History.store",k.stringify(a))},m.intervalList.push(i(m.onUnload,m.options.storeInterval)),m.Adapter.bind(a,"beforeunload",m.onUnload),m.Adapter.bind(a,"unload",m.onUnload));if(!m.emulated.pushState){m.bugs.safariPoll&&m.intervalList.push(i(m.safariStatePoll,m.options.safariPollInterval));if(e.vendor==="Apple Computer, Inc."||(e.appCodeName||"")==="Mozilla")m.Adapter.bind(a,"hashchange",function(){m.Adapter.trigger(a,"popstate")}),m.getHash()&&m.Adapter.onDomLoad(function(){m.Adapter.trigger(a,"hashchange")})}},m.init()}(window) \ No newline at end of file
diff --git a/vendor/assets/javascripts/jquery.nicescroll.js b/vendor/assets/javascripts/jquery.nicescroll.js
new file mode 100644
index 00000000000..7653f25df4b
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.nicescroll.js
@@ -0,0 +1,3634 @@
+/* jquery.nicescroll
+-- version 3.6.0
+-- copyright 2014-11-21 InuYaksa*2014
+-- licensed under the MIT
+--
+-- http://nicescroll.areaaperta.com/
+-- https://github.com/inuyaksa/jquery.nicescroll
+--
+*/
+
+(function(factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as anonymous module.
+ define(['jquery'], factory);
+ } else {
+ // Browser globals.
+ factory(jQuery);
+ }
+}(function(jQuery) {
+ "use strict";
+
+ // globals
+ var domfocus = false;
+ var mousefocus = false;
+ var tabindexcounter = 0;
+ var ascrailcounter = 2000;
+ var globalmaxzindex = 0;
+
+ var $ = jQuery; // sandbox
+
+ // http://stackoverflow.com/questions/2161159/get-script-path
+ function getScriptPath() {
+ var scripts = document.getElementsByTagName('script');
+ var path = scripts[scripts.length - 1].src.split('?')[0];
+ return (path.split('/').length > 0) ? path.split('/').slice(0, -1).join('/') + '/' : '';
+ }
+
+ var vendors = ['webkit','ms','moz','o'];
+
+ var setAnimationFrame = window.requestAnimationFrame || false;
+ var clearAnimationFrame = window.cancelAnimationFrame || false;
+
+ if (!setAnimationFrame) { // legacy detection
+ for (var vx in vendors) {
+ var v = vendors[vx];
+ if (!setAnimationFrame) setAnimationFrame = window[v + 'RequestAnimationFrame'];
+ if (!clearAnimationFrame) clearAnimationFrame = window[v + 'CancelAnimationFrame'] || window[v + 'CancelRequestAnimationFrame'];
+ }
+ }
+
+ var ClsMutationObserver = window.MutationObserver || window.WebKitMutationObserver || false;
+
+ var _globaloptions = {
+ zindex: "auto",
+ cursoropacitymin: 0,
+ cursoropacitymax: 1,
+ cursorcolor: "#424242",
+ cursorwidth: "5px",
+ cursorborder: "1px solid #fff",
+ cursorborderradius: "5px",
+ scrollspeed: 60,
+ mousescrollstep: 8 * 3,
+ touchbehavior: false,
+ hwacceleration: true,
+ usetransition: true,
+ boxzoom: false,
+ dblclickzoom: true,
+ gesturezoom: true,
+ grabcursorenabled: true,
+ autohidemode: true,
+ background: "",
+ iframeautoresize: true,
+ cursorminheight: 32,
+ preservenativescrolling: true,
+ railoffset: false,
+ railhoffset: false,
+ bouncescroll: true,
+ spacebarenabled: true,
+ railpadding: {
+ top: 0,
+ right: 0,
+ left: 0,
+ bottom: 0
+ },
+ disableoutline: true,
+ horizrailenabled: true,
+ railalign: "right",
+ railvalign: "bottom",
+ enabletranslate3d: true,
+ enablemousewheel: true,
+ enablekeyboard: true,
+ smoothscroll: true,
+ sensitiverail: true,
+ enablemouselockapi: true,
+ // cursormaxheight:false,
+ cursorfixedheight: false,
+ directionlockdeadzone: 6,
+ hidecursordelay: 400,
+ nativeparentscrolling: true,
+ enablescrollonselection: true,
+ overflowx: true,
+ overflowy: true,
+ cursordragspeed: 0.3,
+ rtlmode: "auto",
+ cursordragontouch: false,
+ oneaxismousemode: "auto",
+ scriptpath: getScriptPath(),
+ preventmultitouchscrolling: true
+ };
+
+ var browserdetected = false;
+
+ var getBrowserDetection = function() {
+
+ if (browserdetected) return browserdetected;
+
+ var _el = document.createElement('DIV'),
+ _style = _el.style,
+ _agent = navigator.userAgent,
+ _platform = navigator.platform,
+ d = {};
+
+ d.haspointerlock = "pointerLockElement" in document || "webkitPointerLockElement" in document || "mozPointerLockElement" in document;
+
+ d.isopera = ("opera" in window); // 12-
+ d.isopera12 = (d.isopera && ("getUserMedia" in navigator));
+ d.isoperamini = (Object.prototype.toString.call(window.operamini) === "[object OperaMini]");
+
+ d.isie = (("all" in document) && ("attachEvent" in _el) && !d.isopera); //IE10-
+ d.isieold = (d.isie && !("msInterpolationMode" in _style)); // IE6 and older
+ d.isie7 = d.isie && !d.isieold && (!("documentMode" in document) || (document.documentMode == 7));
+ d.isie8 = d.isie && ("documentMode" in document) && (document.documentMode == 8);
+ d.isie9 = d.isie && ("performance" in window) && (document.documentMode >= 9);
+ d.isie10 = d.isie && ("performance" in window) && (document.documentMode == 10);
+ d.isie11 = ("msRequestFullscreen" in _el) && (document.documentMode >= 11); // IE11+
+
+ d.isie9mobile = /iemobile.9/i.test(_agent); //wp 7.1 mango
+ if (d.isie9mobile) d.isie9 = false;
+ d.isie7mobile = (!d.isie9mobile && d.isie7) && /iemobile/i.test(_agent); //wp 7.0
+
+ d.ismozilla = ("MozAppearance" in _style);
+
+ d.iswebkit = ("WebkitAppearance" in _style);
+
+ d.ischrome = ("chrome" in window);
+ d.ischrome22 = (d.ischrome && d.haspointerlock);
+ d.ischrome26 = (d.ischrome && ("transition" in _style)); // issue with transform detection (maintain prefix)
+
+ d.cantouch = ("ontouchstart" in document.documentElement) || ("ontouchstart" in window); // detection for Chrome Touch Emulation
+ d.hasmstouch = (window.MSPointerEvent || false); // IE10 pointer events
+ d.hasw3ctouch = (window.PointerEvent || false); //IE11 pointer events, following W3C Pointer Events spec
+
+ d.ismac = /^mac$/i.test(_platform);
+
+ d.isios = (d.cantouch && /iphone|ipad|ipod/i.test(_platform));
+ d.isios4 = ((d.isios) && !("seal" in Object));
+ d.isios7 = ((d.isios)&&("webkitHidden" in document)); //iOS 7+
+
+ d.isandroid = (/android/i.test(_agent));
+
+ d.haseventlistener = ("addEventListener" in _el);
+
+ d.trstyle = false;
+ d.hastransform = false;
+ d.hastranslate3d = false;
+ d.transitionstyle = false;
+ d.hastransition = false;
+ d.transitionend = false;
+
+ var a;
+ var check = ['transform', 'msTransform', 'webkitTransform', 'MozTransform', 'OTransform'];
+ for (a = 0; a < check.length; a++) {
+ if (typeof _style[check[a]] != "undefined") {
+ d.trstyle = check[a];
+ break;
+ }
+ }
+ d.hastransform = (!!d.trstyle);
+ if (d.hastransform) {
+ _style[d.trstyle] = "translate3d(1px,2px,3px)";
+ d.hastranslate3d = /translate3d/.test(_style[d.trstyle]);
+ }
+
+ d.transitionstyle = false;
+ d.prefixstyle = '';
+ d.transitionend = false;
+ check = ['transition', 'webkitTransition', 'msTransition', 'MozTransition', 'OTransition', 'OTransition', 'KhtmlTransition'];
+ var prefix = ['', '-webkit-', '-ms-', '-moz-', '-o-', '-o', '-khtml-'];
+ var evs = ['transitionend', 'webkitTransitionEnd', 'msTransitionEnd', 'transitionend', 'otransitionend', 'oTransitionEnd', 'KhtmlTransitionEnd'];
+ for (a = 0; a < check.length; a++) {
+ if (check[a] in _style) {
+ d.transitionstyle = check[a];
+ d.prefixstyle = prefix[a];
+ d.transitionend = evs[a];
+ break;
+ }
+ }
+ if (d.ischrome26) { // always use prefix
+ d.prefixstyle = prefix[1];
+ }
+
+ d.hastransition = (d.transitionstyle);
+
+ function detectCursorGrab() {
+ var lst = ['-webkit-grab', '-moz-grab', 'grab'];
+ if ((d.ischrome && !d.ischrome22) || d.isie) lst = []; // force setting for IE returns false positive and chrome cursor bug
+ for (var a = 0; a < lst.length; a++) {
+ var p = lst[a];
+ _style.cursor = p;
+ if (_style.cursor == p) return p;
+ }
+ return 'url(//mail.google.com/mail/images/2/openhand.cur),n-resize'; // thank you google for custom cursor!
+ }
+ d.cursorgrabvalue = detectCursorGrab();
+
+ d.hasmousecapture = ("setCapture" in _el);
+
+ d.hasMutationObserver = (ClsMutationObserver !== false);
+
+ _el = null; //memory released
+
+ browserdetected = d;
+
+ return d;
+ };
+
+ var NiceScrollClass = function(myopt, me) {
+
+ var self = this;
+
+ this.version = '3.6.0';
+ this.name = 'nicescroll';
+
+ this.me = me;
+
+ this.opt = {
+ doc: $("body"),
+ win: false
+ };
+
+ $.extend(this.opt, _globaloptions); // clone opts
+
+ // Options for internal use
+ this.opt.snapbackspeed = 80;
+
+ if (myopt || false) {
+ for (var a in self.opt) {
+ if (typeof myopt[a] != "undefined") self.opt[a] = myopt[a];
+ }
+ }
+
+ this.doc = self.opt.doc;
+ this.iddoc = (this.doc && this.doc[0]) ? this.doc[0].id || '' : '';
+ this.ispage = /^BODY|HTML/.test((self.opt.win) ? self.opt.win[0].nodeName : this.doc[0].nodeName);
+ this.haswrapper = (self.opt.win !== false);
+ this.win = self.opt.win || (this.ispage ? $(window) : this.doc);
+ this.docscroll = (this.ispage && !this.haswrapper) ? $(window) : this.win;
+ this.body = $("body");
+ this.viewport = false;
+
+ this.isfixed = false;
+
+ this.iframe = false;
+ this.isiframe = ((this.doc[0].nodeName == 'IFRAME') && (this.win[0].nodeName == 'IFRAME'));
+
+ this.istextarea = (this.win[0].nodeName == 'TEXTAREA');
+
+ this.forcescreen = false; //force to use screen position on events
+
+ this.canshowonmouseevent = (self.opt.autohidemode != "scroll");
+
+ // Events jump table
+ this.onmousedown = false;
+ this.onmouseup = false;
+ this.onmousemove = false;
+ this.onmousewheel = false;
+ this.onkeypress = false;
+ this.ongesturezoom = false;
+ this.onclick = false;
+
+ // Nicescroll custom events
+ this.onscrollstart = false;
+ this.onscrollend = false;
+ this.onscrollcancel = false;
+
+ this.onzoomin = false;
+ this.onzoomout = false;
+
+ // Let's start!
+ this.view = false;
+ this.page = false;
+
+ this.scroll = {
+ x: 0,
+ y: 0
+ };
+ this.scrollratio = {
+ x: 0,
+ y: 0
+ };
+ this.cursorheight = 20;
+ this.scrollvaluemax = 0;
+
+ this.isrtlmode = (this.opt.rtlmode == "auto") ? ((this.win[0] == window ? this.body : this.win).css("direction") == "rtl") : (this.opt.rtlmode === true);
+ // this.checkrtlmode = false;
+
+ this.scrollrunning = false;
+
+ this.scrollmom = false;
+
+ this.observer = false; // observer div changes
+ this.observerremover = false; // observer on parent for remove detection
+ this.observerbody = false; // observer on body for position change
+
+ do {
+ this.id = "ascrail" + (ascrailcounter++);
+ } while (document.getElementById(this.id));
+
+ this.rail = false;
+ this.cursor = false;
+ this.cursorfreezed = false;
+ this.selectiondrag = false;
+
+ this.zoom = false;
+ this.zoomactive = false;
+
+ this.hasfocus = false;
+ this.hasmousefocus = false;
+
+ this.visibility = true;
+ this.railslocked = false; // locked by resize
+ this.locked = false; // prevent lost of locked status sets by user
+ this.hidden = false; // rails always hidden
+ this.cursoractive = true; // user can interact with cursors
+
+ this.wheelprevented = false; //prevent mousewheel event
+
+ this.overflowx = self.opt.overflowx;
+ this.overflowy = self.opt.overflowy;
+
+ this.nativescrollingarea = false;
+ this.checkarea = 0;
+
+ this.events = []; // event list for unbind
+
+ this.saved = {}; // style saved
+
+ this.delaylist = {};
+ this.synclist = {};
+
+ this.lastdeltax = 0;
+ this.lastdeltay = 0;
+
+ this.detected = getBrowserDetection();
+
+ var cap = $.extend({}, this.detected);
+
+ this.canhwscroll = (cap.hastransform && self.opt.hwacceleration);
+ this.ishwscroll = (this.canhwscroll && self.haswrapper);
+
+ this.hasreversehr = (this.isrtlmode&&!cap.iswebkit); //RTL mode with reverse horizontal axis
+
+ this.istouchcapable = false; // desktop devices with touch screen support
+
+ //## Check WebKit-based desktop with touch support
+ //## + Firefox 18 nightly build (desktop) false positive (or desktop with touch support)
+ if (cap.cantouch && !cap.isios && !cap.isandroid && (cap.iswebkit || cap.ismozilla)) {
+ this.istouchcapable = true;
+ cap.cantouch = false; // parse normal desktop events
+ }
+
+ //## disable MouseLock API on user request
+ if (!self.opt.enablemouselockapi) {
+ cap.hasmousecapture = false;
+ cap.haspointerlock = false;
+ }
+
+/* deprecated
+ this.delayed = function(name, fn, tm, lazy) {
+ };
+*/
+
+ this.debounced = function(name, fn, tm) {
+ var dd = self.delaylist[name];
+ self.delaylist[name] = fn;
+ if (!dd) {
+ setTimeout(function() {
+ var fn = self.delaylist[name];
+ self.delaylist[name] = false;
+ fn.call(self);
+ }, tm);
+ }
+ };
+
+ var _onsync = false;
+
+ this.synched = function(name, fn) {
+
+ function requestSync() {
+ if (_onsync) return;
+ setAnimationFrame(function() {
+ _onsync = false;
+ for (var nn in self.synclist) {
+ var fn = self.synclist[nn];
+ if (fn) fn.call(self);
+ self.synclist[nn] = false;
+ }
+ });
+ _onsync = true;
+ }
+
+ self.synclist[name] = fn;
+ requestSync();
+ return name;
+ };
+
+ this.unsynched = function(name) {
+ if (self.synclist[name]) self.synclist[name] = false;
+ };
+
+ this.css = function(el, pars) { // save & set
+ for (var n in pars) {
+ self.saved.css.push([el, n, el.css(n)]);
+ el.css(n, pars[n]);
+ }
+ };
+
+ this.scrollTop = function(val) {
+ return (typeof val == "undefined") ? self.getScrollTop() : self.setScrollTop(val);
+ };
+
+ this.scrollLeft = function(val) {
+ return (typeof val == "undefined") ? self.getScrollLeft() : self.setScrollLeft(val);
+ };
+
+ // derived by by Dan Pupius www.pupius.net
+ var BezierClass = function(st, ed, spd, p1, p2, p3, p4) {
+
+ this.st = st;
+ this.ed = ed;
+ this.spd = spd;
+
+ this.p1 = p1 || 0;
+ this.p2 = p2 || 1;
+ this.p3 = p3 || 0;
+ this.p4 = p4 || 1;
+
+ this.ts = (new Date()).getTime();
+ this.df = this.ed - this.st;
+ };
+ BezierClass.prototype = {
+ B2: function(t) {
+ return 3 * t * t * (1 - t);
+ },
+ B3: function(t) {
+ return 3 * t * (1 - t) * (1 - t);
+ },
+ B4: function(t) {
+ return (1 - t) * (1 - t) * (1 - t);
+ },
+ getNow: function() {
+ var nw = (new Date()).getTime();
+ var pc = 1 - ((nw - this.ts) / this.spd);
+ var bz = this.B2(pc) + this.B3(pc) + this.B4(pc);
+ return (pc < 0) ? this.ed : this.st + Math.round(this.df * bz);
+ },
+ update: function(ed, spd) {
+ this.st = this.getNow();
+ this.ed = ed;
+ this.spd = spd;
+ this.ts = (new Date()).getTime();
+ this.df = this.ed - this.st;
+ return this;
+ }
+ };
+
+ //derived from http://stackoverflow.com/questions/11236090/
+ function getMatrixValues() {
+ var tr = self.doc.css(cap.trstyle);
+ if (tr && (tr.substr(0, 6) == "matrix")) {
+ return tr.replace(/^.*\((.*)\)$/g, "$1").replace(/px/g, '').split(/, +/);
+ }
+ return false;
+ }
+
+ if (this.ishwscroll) {
+ // hw accelerated scroll
+ this.doc.translate = {
+ x: 0,
+ y: 0,
+ tx: "0px",
+ ty: "0px"
+ };
+
+ //this one can help to enable hw accel on ios6 http://indiegamr.com/ios6-html-hardware-acceleration-changes-and-how-to-fix-them/
+ if (cap.hastranslate3d && cap.isios) this.doc.css("-webkit-backface-visibility", "hidden"); // prevent flickering http://stackoverflow.com/questions/3461441/
+
+ this.getScrollTop = function(last) {
+ if (!last) {
+ var mtx = getMatrixValues();
+ if (mtx) return (mtx.length == 16) ? -mtx[13] : -mtx[5]; //matrix3d 16 on IE10
+ if (self.timerscroll && self.timerscroll.bz) return self.timerscroll.bz.getNow();
+ }
+ return self.doc.translate.y;
+ };
+
+ this.getScrollLeft = function(last) {
+ if (!last) {
+ var mtx = getMatrixValues();
+ if (mtx) return (mtx.length == 16) ? -mtx[12] : -mtx[4]; //matrix3d 16 on IE10
+ if (self.timerscroll && self.timerscroll.bh) return self.timerscroll.bh.getNow();
+ }
+ return self.doc.translate.x;
+ };
+
+ this.notifyScrollEvent = function(el) {
+ var e = document.createEvent("UIEvents");
+ e.initUIEvent("scroll", false, true, window, 1);
+ e.niceevent = true;
+ el.dispatchEvent(e);
+ };
+
+ var cxscrollleft = (this.isrtlmode) ? 1 : -1;
+
+ if (cap.hastranslate3d && self.opt.enabletranslate3d) {
+ this.setScrollTop = function(val, silent) {
+ self.doc.translate.y = val;
+ self.doc.translate.ty = (val * -1) + "px";
+ self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0px)");
+ if (!silent) self.notifyScrollEvent(self.win[0]);
+ };
+ this.setScrollLeft = function(val, silent) {
+ self.doc.translate.x = val;
+ self.doc.translate.tx = (val * cxscrollleft) + "px";
+ self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0px)");
+ if (!silent) self.notifyScrollEvent(self.win[0]);
+ };
+ } else {
+ this.setScrollTop = function(val, silent) {
+ self.doc.translate.y = val;
+ self.doc.translate.ty = (val * -1) + "px";
+ self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")");
+ if (!silent) self.notifyScrollEvent(self.win[0]);
+ };
+ this.setScrollLeft = function(val, silent) {
+ self.doc.translate.x = val;
+ self.doc.translate.tx = (val * cxscrollleft) + "px";
+ self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")");
+ if (!silent) self.notifyScrollEvent(self.win[0]);
+ };
+ }
+ } else {
+ // native scroll
+ this.getScrollTop = function() {
+ return self.docscroll.scrollTop();
+ };
+ this.setScrollTop = function(val) {
+ return self.docscroll.scrollTop(val);
+ };
+ this.getScrollLeft = function() {
+ if (self.detected.ismozilla && self.isrtlmode)
+ return Math.abs(self.docscroll.scrollLeft());
+ return self.docscroll.scrollLeft();
+ };
+ this.setScrollLeft = function(val) {
+ return self.docscroll.scrollLeft((self.detected.ismozilla && self.isrtlmode) ? -val : val);
+ };
+ }
+
+ this.getTarget = function(e) {
+ if (!e) return false;
+ if (e.target) return e.target;
+ if (e.srcElement) return e.srcElement;
+ return false;
+ };
+
+ this.hasParent = function(e, id) {
+ if (!e) return false;
+ var el = e.target || e.srcElement || e || false;
+ while (el && el.id != id) {
+ el = el.parentNode || false;
+ }
+ return (el !== false);
+ };
+
+ function getZIndex() {
+ var dom = self.win;
+ if ("zIndex" in dom) return dom.zIndex(); // use jQuery UI method when available
+ while (dom.length > 0) {
+ if (dom[0].nodeType == 9) return false;
+ var zi = dom.css('zIndex');
+ if (!isNaN(zi) && zi != 0) return parseInt(zi);
+ dom = dom.parent();
+ }
+ return false;
+ }
+
+ //inspired by http://forum.jquery.com/topic/width-includes-border-width-when-set-to-thin-medium-thick-in-ie
+ var _convertBorderWidth = {
+ "thin": 1,
+ "medium": 3,
+ "thick": 5
+ };
+
+ function getWidthToPixel(dom, prop, chkheight) {
+ var wd = dom.css(prop);
+ var px = parseFloat(wd);
+ if (isNaN(px)) {
+ px = _convertBorderWidth[wd] || 0;
+ var brd = (px == 3) ? ((chkheight) ? (self.win.outerHeight() - self.win.innerHeight()) : (self.win.outerWidth() - self.win.innerWidth())) : 1; //DON'T TRUST CSS
+ if (self.isie8 && px) px += 1;
+ return (brd) ? px : 0;
+ }
+ return px;
+ }
+
+ this.getDocumentScrollOffset = function() {
+ return {top:window.pageYOffset||document.documentElement.scrollTop,
+ left:window.pageXOffset||document.documentElement.scrollLeft};
+ }
+
+ this.getOffset = function() {
+ if (self.isfixed) {
+ var ofs = self.win.offset(); // fix Chrome auto issue (when right/bottom props only)
+ var scrl = self.getDocumentScrollOffset();
+ ofs.top-=scrl.top;
+ ofs.left-=scrl.left;
+ return ofs;
+ }
+ var ww = self.win.offset();
+ if (!self.viewport) return ww;
+ var vp = self.viewport.offset();
+ return {
+ top: ww.top - vp.top,// + self.viewport.scrollTop(),
+ left: ww.left - vp.left // + self.viewport.scrollLeft()
+ };
+ };
+
+ this.updateScrollBar = function(len) {
+ if (self.ishwscroll) {
+ self.rail.css({ //**
+ height: self.win.innerHeight() - (self.opt.railpadding.top + self.opt.railpadding.bottom)
+ });
+ if (self.railh) self.railh.css({ //**
+ width: self.win.innerWidth() - (self.opt.railpadding.left + self.opt.railpadding.right)
+ });
+
+ } else {
+ var wpos = self.getOffset();
+ var pos = {
+ top: wpos.top,
+ left: wpos.left - (self.opt.railpadding.left + self.opt.railpadding.right)
+ };
+ pos.top += getWidthToPixel(self.win, 'border-top-width', true);
+ pos.left += (self.rail.align) ? self.win.outerWidth() - getWidthToPixel(self.win, 'border-right-width') - self.rail.width : getWidthToPixel(self.win, 'border-left-width');
+
+ var off = self.opt.railoffset;
+ if (off) {
+ if (off.top) pos.top += off.top;
+ if (self.rail.align && off.left) pos.left += off.left;
+ }
+
+ if (!self.railslocked) self.rail.css({
+ top: pos.top,
+ left: pos.left,
+ height: ((len) ? len.h : self.win.innerHeight()) - (self.opt.railpadding.top + self.opt.railpadding.bottom)
+ });
+
+ if (self.zoom) {
+ self.zoom.css({
+ top: pos.top + 1,
+ left: (self.rail.align == 1) ? pos.left - 20 : pos.left + self.rail.width + 4
+ });
+ }
+
+ if (self.railh && !self.railslocked) {
+ var pos = {
+ top: wpos.top,
+ left: wpos.left
+ };
+ var off = self.opt.railhoffset;
+ if (!!off) {
+ if (!!off.top) pos.top += off.top;
+ if (!!off.left) pos.left += off.left;
+ }
+ var y = (self.railh.align) ? pos.top + getWidthToPixel(self.win, 'border-top-width', true) + self.win.innerHeight() - self.railh.height : pos.top + getWidthToPixel(self.win, 'border-top-width', true);
+ var x = pos.left + getWidthToPixel(self.win, 'border-left-width');
+ self.railh.css({
+ top: y - (self.opt.railpadding.top + self.opt.railpadding.bottom),
+ left: x,
+ width: self.railh.width
+ });
+ }
+
+
+ }
+ };
+
+ this.doRailClick = function(e, dbl, hr) {
+ var fn, pg, cur, pos;
+
+ if (self.railslocked) return;
+ self.cancelEvent(e);
+
+ if (dbl) {
+ fn = (hr) ? self.doScrollLeft : self.doScrollTop;
+ cur = (hr) ? ((e.pageX - self.railh.offset().left - (self.cursorwidth / 2)) * self.scrollratio.x) : ((e.pageY - self.rail.offset().top - (self.cursorheight / 2)) * self.scrollratio.y);
+ fn(cur);
+ } else {
+ fn = (hr) ? self.doScrollLeftBy : self.doScrollBy;
+ cur = (hr) ? self.scroll.x : self.scroll.y;
+ pos = (hr) ? e.pageX - self.railh.offset().left : e.pageY - self.rail.offset().top;
+ pg = (hr) ? self.view.w : self.view.h;
+ fn((cur >= pos) ? pg: -pg);// (cur >= pos) ? fn(pg): fn(-pg);
+ }
+
+ };
+
+ self.hasanimationframe = (setAnimationFrame);
+ self.hascancelanimationframe = (clearAnimationFrame);
+
+ if (!self.hasanimationframe) {
+ setAnimationFrame = function(fn) {
+ return setTimeout(fn, 15 - Math.floor((+new Date()) / 1000) % 16);
+ }; // 1000/60)};
+ clearAnimationFrame = clearInterval;
+ } else if (!self.hascancelanimationframe) clearAnimationFrame = function() {
+ self.cancelAnimationFrame = true;
+ };
+
+ this.init = function() {
+
+ self.saved.css = [];
+
+ if (cap.isie7mobile) return true; // SORRY, DO NOT WORK!
+ if (cap.isoperamini) return true; // SORRY, DO NOT WORK!
+
+ if (cap.hasmstouch) self.css((self.ispage) ? $("html") : self.win, {
+ '-ms-touch-action': 'none'
+ });
+
+ self.zindex = "auto";
+ if (!self.ispage && self.opt.zindex == "auto") {
+ self.zindex = getZIndex() || "auto";
+ } else {
+ self.zindex = self.opt.zindex;
+ }
+
+ if (!self.ispage && self.zindex != "auto") {
+ if (self.zindex > globalmaxzindex) globalmaxzindex = self.zindex;
+ }
+
+ if (self.isie && self.zindex == 0 && self.opt.zindex == "auto") { // fix IE auto == 0
+ self.zindex = "auto";
+ }
+
+ if (!self.ispage || (!cap.cantouch && !cap.isieold && !cap.isie9mobile)) {
+
+ var cont = self.docscroll;
+ if (self.ispage) cont = (self.haswrapper) ? self.win : self.doc;
+
+ if (!cap.isie9mobile) self.css(cont, {
+ 'overflow-y': 'hidden'
+ });
+
+ if (self.ispage && cap.isie7) {
+ if (self.doc[0].nodeName == 'BODY') self.css($("html"), {
+ 'overflow-y': 'hidden'
+ }); //IE7 double scrollbar issue
+ else if (self.doc[0].nodeName == 'HTML') self.css($("body"), {
+ 'overflow-y': 'hidden'
+ }); //IE7 double scrollbar issue
+ }
+
+ if (cap.isios && !self.ispage && !self.haswrapper) self.css($("body"), {
+ "-webkit-overflow-scrolling": "touch"
+ }); //force hw acceleration
+
+ var cursor = $(document.createElement('div'));
+ cursor.css({
+ position: "relative",
+ top: 0,
+ "float": "right",
+ width: self.opt.cursorwidth,
+ height: "0px",
+ 'background-color': self.opt.cursorcolor,
+ border: self.opt.cursorborder,
+ 'background-clip': 'padding-box',
+ '-webkit-border-radius': self.opt.cursorborderradius,
+ '-moz-border-radius': self.opt.cursorborderradius,
+ 'border-radius': self.opt.cursorborderradius
+ });
+
+ cursor.hborder = parseFloat(cursor.outerHeight() - cursor.innerHeight());
+
+ cursor.addClass('nicescroll-cursors');
+
+ self.cursor = cursor;
+
+ var rail = $(document.createElement('div'));
+ rail.attr('id', self.id);
+ rail.addClass('nicescroll-rails nicescroll-rails-vr');
+
+ var v, a, kp = ["left","right","top","bottom"]; //**
+ for (var n in kp) {
+ a = kp[n];
+ v = self.opt.railpadding[a];
+ (v) ? rail.css("padding-"+a,v+"px") : self.opt.railpadding[a] = 0;
+ }
+
+ rail.append(cursor);
+
+ rail.width = Math.max(parseFloat(self.opt.cursorwidth), cursor.outerWidth());
+ rail.css({
+ width: rail.width + "px",
+ 'zIndex': self.zindex,
+ "background": self.opt.background,
+ cursor: "default"
+ });
+
+ rail.visibility = true;
+ rail.scrollable = true;
+
+ rail.align = (self.opt.railalign == "left") ? 0 : 1;
+
+ self.rail = rail;
+
+ self.rail.drag = false;
+
+ var zoom = false;
+ if (self.opt.boxzoom && !self.ispage && !cap.isieold) {
+ zoom = document.createElement('div');
+
+ self.bind(zoom, "click", self.doZoom);
+ self.bind(zoom, "mouseenter", function() {
+ self.zoom.css('opacity', self.opt.cursoropacitymax);
+ });
+ self.bind(zoom, "mouseleave", function() {
+ self.zoom.css('opacity', self.opt.cursoropacitymin);
+ });
+
+ self.zoom = $(zoom);
+ self.zoom.css({
+ "cursor": "pointer",
+ 'z-index': self.zindex,
+ 'backgroundImage': 'url(' + self.opt.scriptpath + 'zoomico.png)',
+ 'height': 18,
+ 'width': 18,
+ 'backgroundPosition': '0px 0px'
+ });
+ if (self.opt.dblclickzoom) self.bind(self.win, "dblclick", self.doZoom);
+ if (cap.cantouch && self.opt.gesturezoom) {
+ self.ongesturezoom = function(e) {
+ if (e.scale > 1.5) self.doZoomIn(e);
+ if (e.scale < 0.8) self.doZoomOut(e);
+ return self.cancelEvent(e);
+ };
+ self.bind(self.win, "gestureend", self.ongesturezoom);
+ }
+ }
+
+ // init HORIZ
+
+ self.railh = false;
+ var railh;
+
+ if (self.opt.horizrailenabled) {
+
+ self.css(cont, {
+ 'overflow-x': 'hidden'
+ });
+
+ var cursor = $(document.createElement('div'));
+ cursor.css({
+ position: "absolute",
+ top: 0,
+ height: self.opt.cursorwidth,
+ width: "0px",
+ 'background-color': self.opt.cursorcolor,
+ border: self.opt.cursorborder,
+ 'background-clip': 'padding-box',
+ '-webkit-border-radius': self.opt.cursorborderradius,
+ '-moz-border-radius': self.opt.cursorborderradius,
+ 'border-radius': self.opt.cursorborderradius
+ });
+
+ if (cap.isieold) cursor.css({'overflow':'hidden'}); //IE6 horiz scrollbar issue
+
+ cursor.wborder = parseFloat(cursor.outerWidth() - cursor.innerWidth());
+
+ cursor.addClass('nicescroll-cursors');
+
+ self.cursorh = cursor;
+
+ railh = $(document.createElement('div'));
+ railh.attr('id', self.id + '-hr');
+ railh.addClass('nicescroll-rails nicescroll-rails-hr');
+ railh.height = Math.max(parseFloat(self.opt.cursorwidth), cursor.outerHeight());
+ railh.css({
+ height: railh.height + "px",
+ 'zIndex': self.zindex,
+ "background": self.opt.background
+ });
+
+ railh.append(cursor);
+
+ railh.visibility = true;
+ railh.scrollable = true;
+
+ railh.align = (self.opt.railvalign == "top") ? 0 : 1;
+
+ self.railh = railh;
+
+ self.railh.drag = false;
+
+ }
+
+ //
+
+ if (self.ispage) {
+ rail.css({
+ position: "fixed",
+ top: "0px",
+ height: "100%"
+ });
+ (rail.align) ? rail.css({
+ right: "0px"
+ }): rail.css({
+ left: "0px"
+ });
+ self.body.append(rail);
+ if (self.railh) {
+ railh.css({
+ position: "fixed",
+ left: "0px",
+ width: "100%"
+ });
+ (railh.align) ? railh.css({
+ bottom: "0px"
+ }): railh.css({
+ top: "0px"
+ });
+ self.body.append(railh);
+ }
+ } else {
+ if (self.ishwscroll) {
+ if (self.win.css('position') == 'static') self.css(self.win, {
+ 'position': 'relative'
+ });
+ var bd = (self.win[0].nodeName == 'HTML') ? self.body : self.win;
+ $(bd).scrollTop(0).scrollLeft(0); // fix rail position if content already scrolled
+ if (self.zoom) {
+ self.zoom.css({
+ position: "absolute",
+ top: 1,
+ right: 0,
+ "margin-right": rail.width + 4
+ });
+ bd.append(self.zoom);
+ }
+ rail.css({
+ position: "absolute",
+ top: 0
+ });
+ (rail.align) ? rail.css({
+ right: 0
+ }): rail.css({
+ left: 0
+ });
+ bd.append(rail);
+ if (railh) {
+ railh.css({
+ position: "absolute",
+ left: 0,
+ bottom: 0
+ });
+ (railh.align) ? railh.css({
+ bottom: 0
+ }): railh.css({
+ top: 0
+ });
+ bd.append(railh);
+ }
+ } else {
+ self.isfixed = (self.win.css("position") == "fixed");
+ var rlpos = (self.isfixed) ? "fixed" : "absolute";
+
+ if (!self.isfixed) self.viewport = self.getViewport(self.win[0]);
+ if (self.viewport) {
+ self.body = self.viewport;
+ if ((/fixed|absolute/.test(self.viewport.css("position"))) == false) self.css(self.viewport, {
+ "position": "relative"
+ });
+ }
+
+ rail.css({
+ position: rlpos
+ });
+ if (self.zoom) self.zoom.css({
+ position: rlpos
+ });
+ self.updateScrollBar();
+ self.body.append(rail);
+ if (self.zoom) self.body.append(self.zoom);
+ if (self.railh) {
+ railh.css({
+ position: rlpos
+ });
+ self.body.append(railh);
+ }
+ }
+
+ if (cap.isios) self.css(self.win, {
+ '-webkit-tap-highlight-color': 'rgba(0,0,0,0)',
+ '-webkit-touch-callout': 'none'
+ }); // prevent grey layer on click
+
+ if (cap.isie && self.opt.disableoutline) self.win.attr("hideFocus", "true"); // IE, prevent dotted rectangle on focused div
+ if (cap.iswebkit && self.opt.disableoutline) self.win.css({"outline": "none"}); // Webkit outline
+ //if (cap.isopera&&self.opt.disableoutline) self.win.css({"outline":"0"}); // Opera 12- to test [TODO]
+
+ }
+
+ if (self.opt.autohidemode === false) {
+ self.autohidedom = false;
+ self.rail.css({
+ opacity: self.opt.cursoropacitymax
+ });
+ if (self.railh) self.railh.css({
+ opacity: self.opt.cursoropacitymax
+ });
+ } else if ((self.opt.autohidemode === true) || (self.opt.autohidemode === "leave")) {
+ self.autohidedom = $().add(self.rail);
+ if (cap.isie8) self.autohidedom = self.autohidedom.add(self.cursor);
+ if (self.railh) self.autohidedom = self.autohidedom.add(self.railh);
+ if (self.railh && cap.isie8) self.autohidedom = self.autohidedom.add(self.cursorh);
+ } else if (self.opt.autohidemode == "scroll") {
+ self.autohidedom = $().add(self.rail);
+ if (self.railh) self.autohidedom = self.autohidedom.add(self.railh);
+ } else if (self.opt.autohidemode == "cursor") {
+ self.autohidedom = $().add(self.cursor);
+ if (self.railh) self.autohidedom = self.autohidedom.add(self.cursorh);
+ } else if (self.opt.autohidemode == "hidden") {
+ self.autohidedom = false;
+ self.hide();
+ self.railslocked = false;
+ }
+
+ if (cap.isie9mobile) {
+
+ self.scrollmom = new ScrollMomentumClass2D(self);
+
+ self.onmangotouch = function() {
+ var py = self.getScrollTop();
+ var px = self.getScrollLeft();
+
+ if ((py == self.scrollmom.lastscrolly) && (px == self.scrollmom.lastscrollx)) return true;
+
+ var dfy = py - self.mangotouch.sy;
+ var dfx = px - self.mangotouch.sx;
+ var df = Math.round(Math.sqrt(Math.pow(dfx, 2) + Math.pow(dfy, 2)));
+ if (df == 0) return;
+
+ var dry = (dfy < 0) ? -1 : 1;
+ var drx = (dfx < 0) ? -1 : 1;
+
+ var tm = +new Date();
+ if (self.mangotouch.lazy) clearTimeout(self.mangotouch.lazy);
+
+ if (((tm - self.mangotouch.tm) > 80) || (self.mangotouch.dry != dry) || (self.mangotouch.drx != drx)) {
+ self.scrollmom.stop();
+ self.scrollmom.reset(px, py);
+ self.mangotouch.sy = py;
+ self.mangotouch.ly = py;
+ self.mangotouch.sx = px;
+ self.mangotouch.lx = px;
+ self.mangotouch.dry = dry;
+ self.mangotouch.drx = drx;
+ self.mangotouch.tm = tm;
+ } else {
+
+ self.scrollmom.stop();
+ self.scrollmom.update(self.mangotouch.sx - dfx, self.mangotouch.sy - dfy);
+ self.mangotouch.tm = tm;
+
+ var ds = Math.max(Math.abs(self.mangotouch.ly - py), Math.abs(self.mangotouch.lx - px));
+ self.mangotouch.ly = py;
+ self.mangotouch.lx = px;
+
+ if (ds > 2) {
+ self.mangotouch.lazy = setTimeout(function() {
+ self.mangotouch.lazy = false;
+ self.mangotouch.dry = 0;
+ self.mangotouch.drx = 0;
+ self.mangotouch.tm = 0;
+ self.scrollmom.doMomentum(30);
+ }, 100);
+ }
+ }
+ };
+
+ var top = self.getScrollTop();
+ var lef = self.getScrollLeft();
+ self.mangotouch = {
+ sy: top,
+ ly: top,
+ dry: 0,
+ sx: lef,
+ lx: lef,
+ drx: 0,
+ lazy: false,
+ tm: 0
+ };
+
+ self.bind(self.docscroll, "scroll", self.onmangotouch);
+
+ } else {
+
+ if (cap.cantouch || self.istouchcapable || self.opt.touchbehavior || cap.hasmstouch) {
+
+ self.scrollmom = new ScrollMomentumClass2D(self);
+
+ self.ontouchstart = function(e) {
+ if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false;
+
+ self.hasmoving = false;
+
+ if (!self.railslocked) {
+
+ var tg;
+ if (cap.hasmstouch) {
+ tg = (e.target) ? e.target : false;
+ while (tg) {
+ var nc = $(tg).getNiceScroll();
+ if ((nc.length > 0) && (nc[0].me == self.me)) break;
+ if (nc.length > 0) return false;
+ if ((tg.nodeName == 'DIV') && (tg.id == self.id)) break;
+ tg = (tg.parentNode) ? tg.parentNode : false;
+ }
+ }
+
+ self.cancelScroll();
+
+ tg = self.getTarget(e);
+
+ if (tg) {
+ var skp = (/INPUT/i.test(tg.nodeName)) && (/range/i.test(tg.type));
+ if (skp) return self.stopPropagation(e);
+ }
+
+ if (!("clientX" in e) && ("changedTouches" in e)) {
+ e.clientX = e.changedTouches[0].clientX;
+ e.clientY = e.changedTouches[0].clientY;
+ }
+
+ if (self.forcescreen) {
+ var le = e;
+ e = {
+ "original": (e.original) ? e.original : e
+ };
+ e.clientX = le.screenX;
+ e.clientY = le.screenY;
+ }
+
+ self.rail.drag = {
+ x: e.clientX,
+ y: e.clientY,
+ sx: self.scroll.x,
+ sy: self.scroll.y,
+ st: self.getScrollTop(),
+ sl: self.getScrollLeft(),
+ pt: 2,
+ dl: false
+ };
+
+ if (self.ispage || !self.opt.directionlockdeadzone) {
+ self.rail.drag.dl = "f";
+ } else {
+
+ var view = {
+ w: $(window).width(),
+ h: $(window).height()
+ };
+
+ var page = {
+ w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth),
+ h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
+ };
+
+ var maxh = Math.max(0, page.h - view.h);
+ var maxw = Math.max(0, page.w - view.w);
+
+ if (!self.rail.scrollable && self.railh.scrollable) self.rail.drag.ck = (maxh > 0) ? "v" : false;
+ else if (self.rail.scrollable && !self.railh.scrollable) self.rail.drag.ck = (maxw > 0) ? "h" : false;
+ else self.rail.drag.ck = false;
+ if (!self.rail.drag.ck) self.rail.drag.dl = "f";
+ }
+
+ if (self.opt.touchbehavior && self.isiframe && cap.isie) {
+ var wp = self.win.position();
+ self.rail.drag.x += wp.left;
+ self.rail.drag.y += wp.top;
+ }
+
+ self.hasmoving = false;
+ self.lastmouseup = false;
+ self.scrollmom.reset(e.clientX, e.clientY);
+
+ if (!cap.cantouch && !this.istouchcapable && !e.pointerType) {
+
+ var ip = (tg) ? /INPUT|SELECT|TEXTAREA/i.test(tg.nodeName) : false;
+ if (!ip) {
+ if (!self.ispage && cap.hasmousecapture) tg.setCapture();
+ if (self.opt.touchbehavior) {
+ if (tg.onclick && !(tg._onclick || false)) { // intercept DOM0 onclick event
+ tg._onclick = tg.onclick;
+ tg.onclick = function(e) {
+ if (self.hasmoving) return false;
+ tg._onclick.call(this, e);
+ };
+ }
+ return self.cancelEvent(e);
+ }
+ return self.stopPropagation(e);
+ }
+
+ if (/SUBMIT|CANCEL|BUTTON/i.test($(tg).attr('type'))) {
+ pc = {
+ "tg": tg,
+ "click": false
+ };
+ self.preventclick = pc;
+ }
+
+ }
+ }
+
+ };
+
+ self.ontouchend = function(e) {
+ if (!self.rail.drag) return true;
+ if (self.rail.drag.pt == 2) {
+ if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false;
+ self.scrollmom.doMomentum();
+ self.rail.drag = false;
+ if (self.hasmoving) {
+ self.lastmouseup = true;
+ self.hideCursor();
+ if (cap.hasmousecapture) document.releaseCapture();
+ if (!cap.cantouch) return self.cancelEvent(e);
+ }
+ }
+ else if (self.rail.drag.pt == 1) {
+ return self.onmouseup(e);
+ }
+
+ };
+
+ var moveneedoffset = (self.opt.touchbehavior && self.isiframe && !cap.hasmousecapture);
+
+ self.ontouchmove = function(e, byiframe) {
+
+ if (!self.rail.drag) return false;
+
+ if (e.targetTouches && self.opt.preventmultitouchscrolling) {
+ if (e.targetTouches.length > 1) return false; // multitouch
+ }
+
+ if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false;
+
+ if (self.rail.drag.pt == 2) {
+ if (cap.cantouch && (cap.isios) && (typeof e.original == "undefined")) return true; // prevent ios "ghost" events by clickable elements
+
+ self.hasmoving = true;
+
+ if (self.preventclick && !self.preventclick.click) {
+ self.preventclick.click = self.preventclick.tg.onclick || false;
+ self.preventclick.tg.onclick = self.onpreventclick;
+ }
+
+ var ev = $.extend({
+ "original": e
+ }, e);
+ e = ev;
+
+ if (("changedTouches" in e)) {
+ e.clientX = e.changedTouches[0].clientX;
+ e.clientY = e.changedTouches[0].clientY;
+ }
+
+ if (self.forcescreen) {
+ var le = e;
+ e = {
+ "original": (e.original) ? e.original : e
+ };
+ e.clientX = le.screenX;
+ e.clientY = le.screenY;
+ }
+
+ var ofy,ofx;
+ ofx = ofy = 0;
+
+ if (moveneedoffset && !byiframe) {
+ var wp = self.win.position();
+ ofx = -wp.left;
+ ofy = -wp.top;
+ }
+
+ var fy = e.clientY + ofy;
+ var my = (fy - self.rail.drag.y);
+ var fx = e.clientX + ofx;
+ var mx = (fx - self.rail.drag.x);
+
+ var ny = self.rail.drag.st - my;
+
+ if (self.ishwscroll && self.opt.bouncescroll) {
+ if (ny < 0) {
+ ny = Math.round(ny / 2);
+ // fy = 0;
+ } else if (ny > self.page.maxh) {
+ ny = self.page.maxh + Math.round((ny - self.page.maxh) / 2);
+ // fy = 0;
+ }
+ } else {
+ if (ny < 0) {
+ ny = 0;
+ fy = 0;
+ }
+ if (ny > self.page.maxh) {
+ ny = self.page.maxh;
+ fy = 0;
+ }
+ }
+
+ var nx;
+ if (self.railh && self.railh.scrollable) {
+ nx = (self.isrtlmode) ? mx - self.rail.drag.sl : self.rail.drag.sl - mx;
+
+ if (self.ishwscroll && self.opt.bouncescroll) {
+ if (nx < 0) {
+ nx = Math.round(nx / 2);
+ // fx = 0;
+ } else if (nx > self.page.maxw) {
+ nx = self.page.maxw + Math.round((nx - self.page.maxw) / 2);
+ // fx = 0;
+ }
+ } else {
+ if (nx < 0) {
+ nx = 0;
+ fx = 0;
+ }
+ if (nx > self.page.maxw) {
+ nx = self.page.maxw;
+ fx = 0;
+ }
+ }
+
+ }
+
+ var grabbed = false;
+ if (self.rail.drag.dl) {
+ grabbed = true;
+ if (self.rail.drag.dl == "v") nx = self.rail.drag.sl;
+ else if (self.rail.drag.dl == "h") ny = self.rail.drag.st;
+ } else {
+ var ay = Math.abs(my);
+ var ax = Math.abs(mx);
+ var dz = self.opt.directionlockdeadzone;
+ if (self.rail.drag.ck == "v") {
+ if (ay > dz && (ax <= (ay * 0.3))) {
+ self.rail.drag = false;
+ return true;
+ } else if (ax > dz) {
+ self.rail.drag.dl = "f";
+ $("body").scrollTop($("body").scrollTop()); // stop iOS native scrolling (when active javascript has blocked)
+ }
+ } else if (self.rail.drag.ck == "h") {
+ if (ax > dz && (ay <= (ax * 0.3))) {
+ self.rail.drag = false;
+ return true;
+ } else if (ay > dz) {
+ self.rail.drag.dl = "f";
+ $("body").scrollLeft($("body").scrollLeft()); // stop iOS native scrolling (when active javascript has blocked)
+ }
+ }
+ }
+
+ self.synched("touchmove", function() {
+ if (self.rail.drag && (self.rail.drag.pt == 2)) {
+ if (self.prepareTransition) self.prepareTransition(0);
+ if (self.rail.scrollable) self.setScrollTop(ny);
+ self.scrollmom.update(fx, fy);
+ if (self.railh && self.railh.scrollable) {
+ self.setScrollLeft(nx);
+ self.showCursor(ny, nx);
+ } else {
+ self.showCursor(ny);
+ }
+ if (cap.isie10) document.selection.clear();
+ }
+ });
+
+ if (cap.ischrome && self.istouchcapable) grabbed = false; //chrome touch emulation doesn't like!
+ if (grabbed) return self.cancelEvent(e);
+ }
+ else if (self.rail.drag.pt == 1) { // drag on cursor
+ return self.onmousemove(e);
+ }
+
+ };
+
+ }
+
+ self.onmousedown = function(e, hronly) {
+ if (self.rail.drag && self.rail.drag.pt != 1) return;
+ if (self.railslocked) return self.cancelEvent(e);
+ self.cancelScroll();
+ self.rail.drag = {
+ x: e.clientX,
+ y: e.clientY,
+ sx: self.scroll.x,
+ sy: self.scroll.y,
+ pt: 1,
+ hr: (!!hronly)
+ };
+ var tg = self.getTarget(e);
+ if (!self.ispage && cap.hasmousecapture) tg.setCapture();
+ if (self.isiframe && !cap.hasmousecapture) {
+ self.saved.csspointerevents = self.doc.css("pointer-events");
+ self.css(self.doc, {
+ "pointer-events": "none"
+ });
+ }
+ self.hasmoving = false;
+ return self.cancelEvent(e);
+ };
+
+ self.onmouseup = function(e) {
+ if (self.rail.drag) {
+ if (self.rail.drag.pt != 1) return true;
+ if (cap.hasmousecapture) document.releaseCapture();
+ if (self.isiframe && !cap.hasmousecapture) self.doc.css("pointer-events", self.saved.csspointerevents);
+ self.rail.drag = false;
+ //if (!self.rail.active) self.hideCursor();
+ if (self.hasmoving) self.triggerScrollEnd(); // TODO - check &&!self.scrollrunning
+ return self.cancelEvent(e);
+ }
+ };
+
+ self.onmousemove = function(e) {
+ if (self.rail.drag) {
+ if (self.rail.drag.pt != 1) return;
+
+ if (cap.ischrome && e.which == 0) return self.onmouseup(e);
+
+ self.cursorfreezed = true;
+ self.hasmoving = true;
+
+ if (self.rail.drag.hr) {
+ self.scroll.x = self.rail.drag.sx + (e.clientX - self.rail.drag.x);
+ if (self.scroll.x < 0) self.scroll.x = 0;
+ var mw = self.scrollvaluemaxw;
+ if (self.scroll.x > mw) self.scroll.x = mw;
+ } else {
+ self.scroll.y = self.rail.drag.sy + (e.clientY - self.rail.drag.y);
+ if (self.scroll.y < 0) self.scroll.y = 0;
+ var my = self.scrollvaluemax;
+ if (self.scroll.y > my) self.scroll.y = my;
+ }
+
+ self.synched('mousemove', function() {
+ if (self.rail.drag && (self.rail.drag.pt == 1)) {
+ self.showCursor();
+ if (self.rail.drag.hr) {
+ if (self.hasreversehr) {
+ self.doScrollLeft(self.scrollvaluemaxw-Math.round(self.scroll.x * self.scrollratio.x), self.opt.cursordragspeed);
+ } else {
+ self.doScrollLeft(Math.round(self.scroll.x * self.scrollratio.x), self.opt.cursordragspeed);
+ }
+ }
+ else self.doScrollTop(Math.round(self.scroll.y * self.scrollratio.y), self.opt.cursordragspeed);
+ }
+ });
+
+ return self.cancelEvent(e);
+ }
+ /*
+ else {
+ self.checkarea = true;
+ }
+*/
+ };
+
+ if (cap.cantouch || self.opt.touchbehavior) {
+
+ self.onpreventclick = function(e) {
+ if (self.preventclick) {
+ self.preventclick.tg.onclick = self.preventclick.click;
+ self.preventclick = false;
+ return self.cancelEvent(e);
+ }
+ }
+
+ self.bind(self.win, "mousedown", self.ontouchstart); // control content dragging
+
+ self.onclick = (cap.isios) ? false : function(e) {
+ if (self.lastmouseup) {
+ self.lastmouseup = false;
+ return self.cancelEvent(e);
+ } else {
+ return true;
+ }
+ };
+
+ if (self.opt.grabcursorenabled && cap.cursorgrabvalue) {
+ self.css((self.ispage) ? self.doc : self.win, {
+ 'cursor': cap.cursorgrabvalue
+ });
+ self.css(self.rail, {
+ 'cursor': cap.cursorgrabvalue
+ });
+ }
+
+ } else {
+
+ var checkSelectionScroll = function(e) {
+ if (!self.selectiondrag) return;
+
+ if (e) {
+ var ww = self.win.outerHeight();
+ var df = (e.pageY - self.selectiondrag.top);
+ if (df > 0 && df < ww) df = 0;
+ if (df >= ww) df -= ww;
+ self.selectiondrag.df = df;
+ }
+ if (self.selectiondrag.df == 0) return;
+
+ var rt = -Math.floor(self.selectiondrag.df / 6) * 2;
+ self.doScrollBy(rt);
+
+ self.debounced("doselectionscroll", function() {
+ checkSelectionScroll()
+ }, 50);
+ };
+
+ if ("getSelection" in document) { // A grade - Major browsers
+ self.hasTextSelected = function() {
+ return (document.getSelection().rangeCount > 0);
+ };
+ } else if ("selection" in document) { //IE9-
+ self.hasTextSelected = function() {
+ return (document.selection.type != "None");
+ };
+ } else {
+ self.hasTextSelected = function() { // no support
+ return false;
+ };
+ }
+
+ self.onselectionstart = function(e) {
+/* More testing - severe chrome issues
+ if (!self.haswrapper&&(e.which&&e.which==2)) { // fool browser to manage middle button scrolling
+ self.win.css({'overflow':'auto'});
+ setTimeout(function(){
+ self.win.css({'overflow':''});
+ },10);
+ return true;
+ }
+*/
+ if (self.ispage) return;
+ self.selectiondrag = self.win.offset();
+ };
+
+ self.onselectionend = function(e) {
+ self.selectiondrag = false;
+ };
+ self.onselectiondrag = function(e) {
+ if (!self.selectiondrag) return;
+ if (self.hasTextSelected()) self.debounced("selectionscroll", function() {
+ checkSelectionScroll(e)
+ }, 250);
+ };
+
+
+ }
+
+ if (cap.hasw3ctouch) { //IE11+
+ self.css(self.rail, {
+ 'touch-action': 'none'
+ });
+ self.css(self.cursor, {
+ 'touch-action': 'none'
+ });
+ self.bind(self.win, "pointerdown", self.ontouchstart);
+ self.bind(document, "pointerup", self.ontouchend);
+ self.bind(document, "pointermove", self.ontouchmove);
+ } else if (cap.hasmstouch) { //IE10
+ self.css(self.rail, {
+ '-ms-touch-action': 'none'
+ });
+ self.css(self.cursor, {
+ '-ms-touch-action': 'none'
+ });
+ self.bind(self.win, "MSPointerDown", self.ontouchstart);
+ self.bind(document, "MSPointerUp", self.ontouchend);
+ self.bind(document, "MSPointerMove", self.ontouchmove);
+ self.bind(self.cursor, "MSGestureHold", function(e) {
+ e.preventDefault()
+ });
+ self.bind(self.cursor, "contextmenu", function(e) {
+ e.preventDefault()
+ });
+ } else if (this.istouchcapable) { //desktop with screen touch enabled
+ self.bind(self.win, "touchstart", self.ontouchstart);
+ self.bind(document, "touchend", self.ontouchend);
+ self.bind(document, "touchcancel", self.ontouchend);
+ self.bind(document, "touchmove", self.ontouchmove);
+ }
+
+
+ if (self.opt.cursordragontouch || (!cap.cantouch && !self.opt.touchbehavior)) {
+
+ self.rail.css({
+ "cursor": "default"
+ });
+ self.railh && self.railh.css({
+ "cursor": "default"
+ });
+
+ self.jqbind(self.rail, "mouseenter", function() {
+ if (!self.ispage && !self.win.is(":visible")) return false;
+ if (self.canshowonmouseevent) self.showCursor();
+ self.rail.active = true;
+ });
+ self.jqbind(self.rail, "mouseleave", function() {
+ self.rail.active = false;
+ if (!self.rail.drag) self.hideCursor();
+ });
+
+ if (self.opt.sensitiverail) {
+ self.bind(self.rail, "click", function(e) {
+ self.doRailClick(e, false, false)
+ });
+ self.bind(self.rail, "dblclick", function(e) {
+ self.doRailClick(e, true, false)
+ });
+ self.bind(self.cursor, "click", function(e) {
+ self.cancelEvent(e)
+ });
+ self.bind(self.cursor, "dblclick", function(e) {
+ self.cancelEvent(e)
+ });
+ }
+
+ if (self.railh) {
+ self.jqbind(self.railh, "mouseenter", function() {
+ if (!self.ispage && !self.win.is(":visible")) return false;
+ if (self.canshowonmouseevent) self.showCursor();
+ self.rail.active = true;
+ });
+ self.jqbind(self.railh, "mouseleave", function() {
+ self.rail.active = false;
+ if (!self.rail.drag) self.hideCursor();
+ });
+
+ if (self.opt.sensitiverail) {
+ self.bind(self.railh, "click", function(e) {
+ self.doRailClick(e, false, true)
+ });
+ self.bind(self.railh, "dblclick", function(e) {
+ self.doRailClick(e, true, true)
+ });
+ self.bind(self.cursorh, "click", function(e) {
+ self.cancelEvent(e)
+ });
+ self.bind(self.cursorh, "dblclick", function(e) {
+ self.cancelEvent(e)
+ });
+ }
+
+ }
+
+ }
+
+ if (!cap.cantouch && !self.opt.touchbehavior) {
+
+ self.bind((cap.hasmousecapture) ? self.win : document, "mouseup", self.onmouseup);
+ self.bind(document, "mousemove", self.onmousemove);
+ if (self.onclick) self.bind(document, "click", self.onclick);
+
+ self.bind(self.cursor, "mousedown", self.onmousedown);
+ self.bind(self.cursor, "mouseup", self.onmouseup);
+
+ if (self.railh) {
+ self.bind(self.cursorh, "mousedown", function(e) {
+ self.onmousedown(e, true)
+ });
+ self.bind(self.cursorh, "mouseup", self.onmouseup);
+ }
+
+ if (!self.ispage && self.opt.enablescrollonselection) {
+ self.bind(self.win[0], "mousedown", self.onselectionstart);
+ self.bind(document, "mouseup", self.onselectionend);
+ self.bind(self.cursor, "mouseup", self.onselectionend);
+ if (self.cursorh) self.bind(self.cursorh, "mouseup", self.onselectionend);
+ self.bind(document, "mousemove", self.onselectiondrag);
+ }
+
+ if (self.zoom) {
+ self.jqbind(self.zoom, "mouseenter", function() {
+ if (self.canshowonmouseevent) self.showCursor();
+ self.rail.active = true;
+ });
+ self.jqbind(self.zoom, "mouseleave", function() {
+ self.rail.active = false;
+ if (!self.rail.drag) self.hideCursor();
+ });
+ }
+
+ } else {
+
+ self.bind((cap.hasmousecapture) ? self.win : document, "mouseup", self.ontouchend);
+ self.bind(document, "mousemove", self.ontouchmove);
+ if (self.onclick) self.bind(document, "click", self.onclick);
+
+ if (self.opt.cursordragontouch) {
+ self.bind(self.cursor, "mousedown", self.onmousedown);
+ self.bind(self.cursor, "mouseup", self.onmouseup);
+ //self.bind(self.cursor, "mousemove", self.onmousemove);
+ self.cursorh && self.bind(self.cursorh, "mousedown", function(e) {
+ self.onmousedown(e, true)
+ });
+ //self.cursorh && self.bind(self.cursorh, "mousemove", self.onmousemove);
+ self.cursorh && self.bind(self.cursorh, "mouseup", self.onmouseup);
+ }
+
+ }
+
+ if (self.opt.enablemousewheel) {
+ if (!self.isiframe) self.bind((cap.isie && self.ispage) ? document : self.win /*self.docscroll*/ , "mousewheel", self.onmousewheel);
+ self.bind(self.rail, "mousewheel", self.onmousewheel);
+ if (self.railh) self.bind(self.railh, "mousewheel", self.onmousewheelhr);
+ }
+
+ if (!self.ispage && !cap.cantouch && !(/HTML|^BODY/.test(self.win[0].nodeName))) {
+ if (!self.win.attr("tabindex")) self.win.attr({
+ "tabindex": tabindexcounter++
+ });
+
+ self.jqbind(self.win, "focus", function(e) {
+ domfocus = (self.getTarget(e)).id || true;
+ self.hasfocus = true;
+ if (self.canshowonmouseevent) self.noticeCursor();
+ });
+ self.jqbind(self.win, "blur", function(e) {
+ domfocus = false;
+ self.hasfocus = false;
+ });
+
+ self.jqbind(self.win, "mouseenter", function(e) {
+ mousefocus = (self.getTarget(e)).id || true;
+ self.hasmousefocus = true;
+ if (self.canshowonmouseevent) self.noticeCursor();
+ });
+ self.jqbind(self.win, "mouseleave", function() {
+ mousefocus = false;
+ self.hasmousefocus = false;
+ if (!self.rail.drag) self.hideCursor();
+ });
+
+ }
+
+ } // !ie9mobile
+
+ //Thanks to http://www.quirksmode.org !!
+ self.onkeypress = function(e) {
+ if (self.railslocked && self.page.maxh == 0) return true;
+
+ e = (e) ? e : window.e;
+ var tg = self.getTarget(e);
+ if (tg && /INPUT|TEXTAREA|SELECT|OPTION/.test(tg.nodeName)) {
+ var tp = tg.getAttribute('type') || tg.type || false;
+ if ((!tp) || !(/submit|button|cancel/i.tp)) return true;
+ }
+
+ if ($(tg).attr('contenteditable')) return true;
+
+ if (self.hasfocus || (self.hasmousefocus && !domfocus) || (self.ispage && !domfocus && !mousefocus)) {
+ var key = e.keyCode;
+
+ if (self.railslocked && key != 27) return self.cancelEvent(e);
+
+ var ctrl = e.ctrlKey || false;
+ var shift = e.shiftKey || false;
+
+ var ret = false;
+ switch (key) {
+ case 38:
+ case 63233: //safari
+ self.doScrollBy(24 * 3);
+ ret = true;
+ break;
+ case 40:
+ case 63235: //safari
+ self.doScrollBy(-24 * 3);
+ ret = true;
+ break;
+ case 37:
+ case 63232: //safari
+ if (self.railh) {
+ (ctrl) ? self.doScrollLeft(0): self.doScrollLeftBy(24 * 3);
+ ret = true;
+ }
+ break;
+ case 39:
+ case 63234: //safari
+ if (self.railh) {
+ (ctrl) ? self.doScrollLeft(self.page.maxw): self.doScrollLeftBy(-24 * 3);
+ ret = true;
+ }
+ break;
+ case 33:
+ case 63276: // safari
+ self.doScrollBy(self.view.h);
+ ret = true;
+ break;
+ case 34:
+ case 63277: // safari
+ self.doScrollBy(-self.view.h);
+ ret = true;
+ break;
+ case 36:
+ case 63273: // safari
+ (self.railh && ctrl) ? self.doScrollPos(0, 0): self.doScrollTo(0);
+ ret = true;
+ break;
+ case 35:
+ case 63275: // safari
+ (self.railh && ctrl) ? self.doScrollPos(self.page.maxw, self.page.maxh): self.doScrollTo(self.page.maxh);
+ ret = true;
+ break;
+ case 32:
+ if (self.opt.spacebarenabled) {
+ (shift) ? self.doScrollBy(self.view.h): self.doScrollBy(-self.view.h);
+ ret = true;
+ }
+ break;
+ case 27: // ESC
+ if (self.zoomactive) {
+ self.doZoom();
+ ret = true;
+ }
+ break;
+ }
+ if (ret) return self.cancelEvent(e);
+ }
+ };
+
+ if (self.opt.enablekeyboard) self.bind(document, (cap.isopera && !cap.isopera12) ? "keypress" : "keydown", self.onkeypress);
+
+ self.bind(document, "keydown", function(e) {
+ var ctrl = e.ctrlKey || false;
+ if (ctrl) self.wheelprevented = true;
+ });
+ self.bind(document, "keyup", function(e) {
+ var ctrl = e.ctrlKey || false;
+ if (!ctrl) self.wheelprevented = false;
+ });
+ self.bind(window,"blur",function(e){
+ self.wheelprevented = false;
+ });
+
+ self.bind(window, 'resize', self.lazyResize);
+ self.bind(window, 'orientationchange', self.lazyResize);
+
+ self.bind(window, "load", self.lazyResize);
+
+ if (cap.ischrome && !self.ispage && !self.haswrapper) { //chrome void scrollbar bug - it persists in version 26
+ var tmp = self.win.attr("style");
+ var ww = parseFloat(self.win.css("width")) + 1;
+ self.win.css('width', ww);
+ self.synched("chromefix", function() {
+ self.win.attr("style", tmp)
+ });
+ }
+
+
+ // Trying a cross-browser implementation - good luck!
+
+ self.onAttributeChange = function(e) {
+ self.lazyResize(self.isieold ? 250 : 30);
+ };
+
+ if (ClsMutationObserver !== false) {
+ self.observerbody = new ClsMutationObserver(function(mutations) {
+ mutations.forEach(function(mut){
+ if (mut.type=="attributes") {
+ return ($("body").hasClass("modal-open")) ? self.hide() : self.show(); // Support for Bootstrap modal
+ }
+ });
+ if (document.body.scrollHeight!=self.page.maxh) return self.lazyResize(30);
+ });
+ self.observerbody.observe(document.body, {
+ childList: true,
+ subtree: true,
+ characterData: false,
+ attributes: true,
+ attributeFilter: ['class']
+ });
+ }
+
+ if (!self.ispage && !self.haswrapper) {
+ // redesigned MutationObserver for Chrome18+/Firefox14+/iOS6+ with support for: remove div, add/remove content
+ if (ClsMutationObserver !== false) {
+ self.observer = new ClsMutationObserver(function(mutations) {
+ mutations.forEach(self.onAttributeChange);
+ });
+ self.observer.observe(self.win[0], {
+ childList: true,
+ characterData: false,
+ attributes: true,
+ subtree: false
+ });
+ self.observerremover = new ClsMutationObserver(function(mutations) {
+ mutations.forEach(function(mo) {
+ if (mo.removedNodes.length > 0) {
+ for (var dd in mo.removedNodes) {
+ if (!!self && (mo.removedNodes[dd] == self.win[0])) return self.remove();
+ }
+ }
+ });
+ });
+ self.observerremover.observe(self.win[0].parentNode, {
+ childList: true,
+ characterData: false,
+ attributes: false,
+ subtree: false
+ });
+ } else {
+ self.bind(self.win, (cap.isie && !cap.isie9) ? "propertychange" : "DOMAttrModified", self.onAttributeChange);
+ if (cap.isie9) self.win[0].attachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug
+ self.bind(self.win, "DOMNodeRemoved", function(e) {
+ if (e.target == self.win[0]) self.remove();
+ });
+ }
+ }
+
+ //
+
+ if (!self.ispage && self.opt.boxzoom) self.bind(window, "resize", self.resizeZoom);
+ if (self.istextarea) self.bind(self.win, "mouseup", self.lazyResize);
+
+ // self.checkrtlmode = true;
+ self.lazyResize(30);
+
+ }
+
+ if (this.doc[0].nodeName == 'IFRAME') {
+ var oniframeload = function() {
+ self.iframexd = false;
+ var doc;
+ try {
+ doc = 'contentDocument' in this ? this.contentDocument : this.contentWindow.document;
+ var a = doc.domain;
+ } catch (e) {
+ self.iframexd = true;
+ doc = false
+ }
+
+ if (self.iframexd) {
+ if ("console" in window) console.log('NiceScroll error: policy restriced iframe');
+ return true; //cross-domain - I can't manage this
+ }
+
+ self.forcescreen = true;
+
+ if (self.isiframe) {
+ self.iframe = {
+ "doc": $(doc),
+ "html": self.doc.contents().find('html')[0],
+ "body": self.doc.contents().find('body')[0]
+ };
+ self.getContentSize = function() {
+ return {
+ w: Math.max(self.iframe.html.scrollWidth, self.iframe.body.scrollWidth),
+ h: Math.max(self.iframe.html.scrollHeight, self.iframe.body.scrollHeight)
+ };
+ };
+ self.docscroll = $(self.iframe.body); //$(this.contentWindow);
+ }
+
+ if (!cap.isios && self.opt.iframeautoresize && !self.isiframe) {
+ self.win.scrollTop(0); // reset position
+ self.doc.height(""); //reset height to fix browser bug
+ var hh = Math.max(doc.getElementsByTagName('html')[0].scrollHeight, doc.body.scrollHeight);
+ self.doc.height(hh);
+ }
+ self.lazyResize(30);
+
+ if (cap.isie7) self.css($(self.iframe.html), {
+ 'overflow-y': 'hidden'
+ });
+ self.css($(self.iframe.body), {
+ 'overflow-y': 'hidden'
+ });
+
+ if (cap.isios && self.haswrapper) {
+ self.css($(doc.body), {
+ '-webkit-transform': 'translate3d(0,0,0)'
+ }); // avoid iFrame content clipping - thanks to http://blog.derraab.com/2012/04/02/avoid-iframe-content-clipping-with-css-transform-on-ios/
+ }
+
+ if ('contentWindow' in this) {
+ self.bind(this.contentWindow, "scroll", self.onscroll); //IE8 & minor
+ } else {
+ self.bind(doc, "scroll", self.onscroll);
+ }
+
+ if (self.opt.enablemousewheel) {
+ self.bind(doc, "mousewheel", self.onmousewheel);
+ }
+
+ if (self.opt.enablekeyboard) self.bind(doc, (cap.isopera) ? "keypress" : "keydown", self.onkeypress);
+
+ if (cap.cantouch || self.opt.touchbehavior) {
+ self.bind(doc, "mousedown", self.ontouchstart);
+ self.bind(doc, "mousemove", function(e) {
+ return self.ontouchmove(e, true)
+ });
+ if (self.opt.grabcursorenabled && cap.cursorgrabvalue) self.css($(doc.body), {
+ 'cursor': cap.cursorgrabvalue
+ });
+ }
+
+ self.bind(doc, "mouseup", self.ontouchend);
+
+ if (self.zoom) {
+ if (self.opt.dblclickzoom) self.bind(doc, 'dblclick', self.doZoom);
+ if (self.ongesturezoom) self.bind(doc, "gestureend", self.ongesturezoom);
+ }
+ };
+
+ if (this.doc[0].readyState && this.doc[0].readyState == "complete") {
+ setTimeout(function() {
+ oniframeload.call(self.doc[0], false)
+ }, 500);
+ }
+ self.bind(this.doc, "load", oniframeload);
+
+ }
+
+ };
+
+ this.showCursor = function(py, px) {
+ if (self.cursortimeout) {
+ clearTimeout(self.cursortimeout);
+ self.cursortimeout = 0;
+ }
+ if (!self.rail) return;
+ if (self.autohidedom) {
+ self.autohidedom.stop().css({
+ opacity: self.opt.cursoropacitymax
+ });
+ self.cursoractive = true;
+ }
+
+ if (!self.rail.drag || self.rail.drag.pt != 1) {
+ if ((typeof py != "undefined") && (py !== false)) {
+ self.scroll.y = Math.round(py * 1 / self.scrollratio.y);
+ }
+ if (typeof px != "undefined") {
+ self.scroll.x = Math.round(px * 1 / self.scrollratio.x);
+ }
+ }
+
+ self.cursor.css({
+ height: self.cursorheight,
+ top: self.scroll.y
+ });
+ if (self.cursorh) {
+ var lx = (self.hasreversehr) ? self.scrollvaluemaxw-self.scroll.x : self.scroll.x;
+ (!self.rail.align && self.rail.visibility) ? self.cursorh.css({
+ width: self.cursorwidth,
+ left: lx + self.rail.width
+ }): self.cursorh.css({
+ width: self.cursorwidth,
+ left: lx
+ });
+ self.cursoractive = true;
+ }
+
+ if (self.zoom) self.zoom.stop().css({
+ opacity: self.opt.cursoropacitymax
+ });
+ };
+
+ this.hideCursor = function(tm) {
+ if (self.cursortimeout) return;
+ if (!self.rail) return;
+ if (!self.autohidedom) return;
+ if (self.hasmousefocus && self.opt.autohidemode == "leave") return;
+ self.cursortimeout = setTimeout(function() {
+ if (!self.rail.active || !self.showonmouseevent) {
+ self.autohidedom.stop().animate({
+ opacity: self.opt.cursoropacitymin
+ });
+ if (self.zoom) self.zoom.stop().animate({
+ opacity: self.opt.cursoropacitymin
+ });
+ self.cursoractive = false;
+ }
+ self.cursortimeout = 0;
+ }, tm || self.opt.hidecursordelay);
+ };
+
+ this.noticeCursor = function(tm, py, px) {
+ self.showCursor(py, px);
+ if (!self.rail.active) self.hideCursor(tm);
+ };
+
+ this.getContentSize =
+ (self.ispage) ?
+ function() {
+ return {
+ w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth),
+ h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
+ }
+ } : (self.haswrapper) ?
+ function() {
+ return {
+ w: self.doc.outerWidth() + parseInt(self.win.css('paddingLeft')) + parseInt(self.win.css('paddingRight')),
+ h: self.doc.outerHeight() + parseInt(self.win.css('paddingTop')) + parseInt(self.win.css('paddingBottom'))
+ }
+ } : function() {
+ return {
+ w: self.docscroll[0].scrollWidth,
+ h: self.docscroll[0].scrollHeight
+ }
+ };
+
+ this.onResize = function(e, page) {
+
+ if (!self || !self.win) return false;
+
+ if (!self.haswrapper && !self.ispage) {
+ if (self.win.css('display') == 'none') {
+ if (self.visibility) self.hideRail().hideRailHr();
+ return false;
+ } else {
+ if (!self.hidden && !self.visibility) self.showRail().showRailHr();
+ }
+ }
+
+ var premaxh = self.page.maxh;
+ var premaxw = self.page.maxw;
+
+ var preview = {
+ h: self.view.h,
+ w: self.view.w
+ };
+
+ self.view = {
+ w: (self.ispage) ? self.win.width() : parseInt(self.win[0].clientWidth),
+ h: (self.ispage) ? self.win.height() : parseInt(self.win[0].clientHeight)
+ };
+
+ self.page = (page) ? page : self.getContentSize();
+
+ self.page.maxh = Math.max(0, self.page.h - self.view.h);
+ self.page.maxw = Math.max(0, self.page.w - self.view.w);
+
+ if ((self.page.maxh == premaxh) && (self.page.maxw == premaxw) && (self.view.w == preview.w) && (self.view.h == preview.h)) {
+ // test position
+ if (!self.ispage) {
+ var pos = self.win.offset();
+ if (self.lastposition) {
+ var lst = self.lastposition;
+ if ((lst.top == pos.top) && (lst.left == pos.left)) return self; //nothing to do
+ }
+ self.lastposition = pos;
+ } else {
+ return self; //nothing to do
+ }
+ }
+
+ if (self.page.maxh == 0) {
+ self.hideRail();
+ self.scrollvaluemax = 0;
+ self.scroll.y = 0;
+ self.scrollratio.y = 0;
+ self.cursorheight = 0;
+ self.setScrollTop(0);
+ self.rail.scrollable = false;
+ } else {
+ self.page.maxh -= (self.opt.railpadding.top + self.opt.railpadding.bottom); //**
+ self.rail.scrollable = true;
+ }
+
+ if (self.page.maxw == 0) {
+ self.hideRailHr();
+ self.scrollvaluemaxw = 0;
+ self.scroll.x = 0;
+ self.scrollratio.x = 0;
+ self.cursorwidth = 0;
+ self.setScrollLeft(0);
+ self.railh.scrollable = false;
+ } else {
+ self.page.maxw -= (self.opt.railpadding.left + self.opt.railpadding.right); //**
+ self.railh.scrollable = true;
+ }
+
+ self.railslocked = (self.locked) || ((self.page.maxh == 0) && (self.page.maxw == 0));
+ if (self.railslocked) {
+ if (!self.ispage) self.updateScrollBar(self.view);
+ return false;
+ }
+
+ if (!self.hidden && !self.visibility) {
+ self.showRail().showRailHr();
+ }
+ else if (!self.hidden && !self.railh.visibility) self.showRailHr();
+
+ if (self.istextarea && self.win.css('resize') && self.win.css('resize') != 'none') self.view.h -= 20;
+
+ self.cursorheight = Math.min(self.view.h, Math.round(self.view.h * (self.view.h / self.page.h)));
+ self.cursorheight = (self.opt.cursorfixedheight) ? self.opt.cursorfixedheight : Math.max(self.opt.cursorminheight, self.cursorheight);
+
+ self.cursorwidth = Math.min(self.view.w, Math.round(self.view.w * (self.view.w / self.page.w)));
+ self.cursorwidth = (self.opt.cursorfixedheight) ? self.opt.cursorfixedheight : Math.max(self.opt.cursorminheight, self.cursorwidth);
+
+ self.scrollvaluemax = self.view.h - self.cursorheight - self.cursor.hborder - (self.opt.railpadding.top + self.opt.railpadding.bottom); //**
+
+ if (self.railh) {
+ self.railh.width = (self.page.maxh > 0) ? (self.view.w - self.rail.width) : self.view.w;
+ self.scrollvaluemaxw = self.railh.width - self.cursorwidth - self.cursorh.wborder - (self.opt.railpadding.left + self.opt.railpadding.right); //**
+ }
+
+ /*
+ if (self.checkrtlmode&&self.railh) {
+ self.checkrtlmode = false;
+ if (self.opt.rtlmode&&self.scroll.x==0) self.setScrollLeft(self.page.maxw);
+ }
+*/
+
+ if (!self.ispage) self.updateScrollBar(self.view);
+
+ self.scrollratio = {
+ x: (self.page.maxw / self.scrollvaluemaxw),
+ y: (self.page.maxh / self.scrollvaluemax)
+ };
+
+ var sy = self.getScrollTop();
+ if (sy > self.page.maxh) {
+ self.doScrollTop(self.page.maxh);
+ } else {
+ self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y));
+ self.scroll.x = Math.round(self.getScrollLeft() * (1 / self.scrollratio.x));
+ if (self.cursoractive) self.noticeCursor();
+ }
+
+ if (self.scroll.y && (self.getScrollTop() == 0)) self.doScrollTo(Math.floor(self.scroll.y * self.scrollratio.y));
+
+ return self;
+ };
+
+ this.resize = self.onResize;
+
+ this.lazyResize = function(tm) { // event debounce
+ tm = (isNaN(tm)) ? 30 : tm;
+ self.debounced('resize', self.resize, tm);
+ return self;
+ };
+
+ // modified by MDN https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/wheel
+ function _modernWheelEvent(dom, name, fn, bubble) {
+ self._bind(dom, name, function(e) {
+ var e = (e) ? e : window.event;
+ var event = {
+ original: e,
+ target: e.target || e.srcElement,
+ type: "wheel",
+ deltaMode: e.type == "MozMousePixelScroll" ? 0 : 1,
+ deltaX: 0,
+ deltaZ: 0,
+ preventDefault: function() {
+ e.preventDefault ? e.preventDefault() : e.returnValue = false;
+ return false;
+ },
+ stopImmediatePropagation: function() {
+ (e.stopImmediatePropagation) ? e.stopImmediatePropagation(): e.cancelBubble = true;
+ }
+ };
+
+ if (name == "mousewheel") {
+ event.deltaY = -1 / 40 * e.wheelDelta;
+ e.wheelDeltaX && (event.deltaX = -1 / 40 * e.wheelDeltaX);
+ } else {
+ event.deltaY = e.detail;
+ }
+
+ return fn.call(dom, event);
+ }, bubble);
+ };
+
+
+
+ this.jqbind = function(dom, name, fn) { // use jquery bind for non-native events (mouseenter/mouseleave)
+ self.events.push({
+ e: dom,
+ n: name,
+ f: fn,
+ q: true
+ });
+ $(dom).bind(name, fn);
+ };
+
+ this.bind = function(dom, name, fn, bubble) { // touch-oriented & fixing jquery bind
+ var el = ("jquery" in dom) ? dom[0] : dom;
+
+ if (name == 'mousewheel') {
+ if (window.addEventListener||'onwheel' in document) { // modern brosers & IE9 detection fix
+ self._bind(el, "wheel", fn, bubble || false);
+ } else {
+ var wname = (typeof document.onmousewheel != "undefined") ? "mousewheel" : "DOMMouseScroll"; // older IE/Firefox
+ _modernWheelEvent(el, wname, fn, bubble || false);
+ if (wname == "DOMMouseScroll") _modernWheelEvent(el, "MozMousePixelScroll", fn, bubble || false); // Firefox legacy
+ }
+ } else if (el.addEventListener) {
+ if (cap.cantouch && /mouseup|mousedown|mousemove/.test(name)) { // touch device support
+ var tt = (name == 'mousedown') ? 'touchstart' : (name == 'mouseup') ? 'touchend' : 'touchmove';
+ self._bind(el, tt, function(e) {
+ if (e.touches) {
+ if (e.touches.length < 2) {
+ var ev = (e.touches.length) ? e.touches[0] : e;
+ ev.original = e;
+ fn.call(this, ev);
+ }
+ } else if (e.changedTouches) {
+ var ev = e.changedTouches[0];
+ ev.original = e;
+ fn.call(this, ev);
+ } //blackberry
+ }, bubble || false);
+ }
+ self._bind(el, name, fn, bubble || false);
+ if (cap.cantouch && name == "mouseup") self._bind(el, "touchcancel", fn, bubble || false);
+ } else {
+ self._bind(el, name, function(e) {
+ e = e || window.event || false;
+ if (e) {
+ if (e.srcElement) e.target = e.srcElement;
+ }
+ if (!("pageY" in e)) {
+ e.pageX = e.clientX + document.documentElement.scrollLeft;
+ e.pageY = e.clientY + document.documentElement.scrollTop;
+ }
+ return ((fn.call(el, e) === false) || bubble === false) ? self.cancelEvent(e) : true;
+ });
+ }
+ };
+
+ if (cap.haseventlistener) { // W3C standard model
+ this._bind = function(el, name, fn, bubble) { // primitive bind
+ self.events.push({
+ e: el,
+ n: name,
+ f: fn,
+ b: bubble,
+ q: false
+ });
+ el.addEventListener(name, fn, bubble || false);
+ };
+ this.cancelEvent = function(e) {
+ if (!e) return false;
+ var e = (e.original) ? e.original : e;
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.preventManipulation) e.preventManipulation(); //IE10
+ return false;
+ };
+ this.stopPropagation = function(e) {
+ if (!e) return false;
+ var e = (e.original) ? e.original : e;
+ e.stopPropagation();
+ return false;
+ };
+ this._unbind = function(el, name, fn, bub) { // primitive unbind
+ el.removeEventListener(name, fn, bub);
+ };
+ } else { // old IE model
+ this._bind = function(el, name, fn, bubble) { // primitive bind
+ self.events.push({
+ e: el,
+ n: name,
+ f: fn,
+ b: bubble,
+ q: false
+ });
+ if (el.attachEvent) {
+ el.attachEvent("on" + name, fn);
+ } else {
+ el["on" + name] = fn;
+ }
+ };
+ // Thanks to http://www.switchonthecode.com !!
+ this.cancelEvent = function(e) {
+ var e = window.event || false;
+ if (!e) return false;
+ e.cancelBubble = true;
+ e.cancel = true;
+ e.returnValue = false;
+ return false;
+ };
+ this.stopPropagation = function(e) {
+ var e = window.event || false;
+ if (!e) return false;
+ e.cancelBubble = true;
+ return false;
+ };
+ this._unbind = function(el, name, fn, bub) { // primitive unbind IE old
+ if (el.detachEvent) {
+ el.detachEvent('on' + name, fn);
+ } else {
+ el['on' + name] = false;
+ }
+ };
+ }
+
+ this.unbindAll = function() {
+ for (var a = 0; a < self.events.length; a++) {
+ var r = self.events[a];
+ (r.q) ? r.e.unbind(r.n, r.f): self._unbind(r.e, r.n, r.f, r.b);
+ }
+ };
+
+ this.showRail = function() {
+ if ((self.page.maxh != 0) && (self.ispage || self.win.css('display') != 'none')) {
+ self.visibility = true;
+ self.rail.visibility = true;
+ self.rail.css('display', 'block');
+ }
+ return self;
+ };
+
+ this.showRailHr = function() {
+ if (!self.railh) return self;
+ if ((self.page.maxw != 0) && (self.ispage || self.win.css('display') != 'none')) {
+ self.railh.visibility = true;
+ self.railh.css('display', 'block');
+ }
+ return self;
+ };
+
+ this.hideRail = function() {
+ self.visibility = false;
+ self.rail.visibility = false;
+ self.rail.css('display', 'none');
+ return self;
+ };
+
+ this.hideRailHr = function() {
+ if (!self.railh) return self;
+ self.railh.visibility = false;
+ self.railh.css('display', 'none');
+ return self;
+ };
+
+ this.show = function() {
+ self.hidden = false;
+ self.railslocked = false;
+ return self.showRail().showRailHr();
+ };
+
+ this.hide = function() {
+ self.hidden = true;
+ self.railslocked = true;
+ return self.hideRail().hideRailHr();
+ };
+
+ this.toggle = function() {
+ return (self.hidden) ? self.show() : self.hide();
+ };
+
+ this.remove = function() {
+ self.stop();
+ if (self.cursortimeout) clearTimeout(self.cursortimeout);
+ self.doZoomOut();
+ self.unbindAll();
+
+ if (cap.isie9) self.win[0].detachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug
+
+ if (self.observer !== false) self.observer.disconnect();
+ if (self.observerremover !== false) self.observerremover.disconnect();
+ if (self.observerbody !== false) self.observerbody.disconnect();
+
+ self.events = null;
+
+ if (self.cursor) {
+ self.cursor.remove();
+ }
+ if (self.cursorh) {
+ self.cursorh.remove();
+ }
+ if (self.rail) {
+ self.rail.remove();
+ }
+ if (self.railh) {
+ self.railh.remove();
+ }
+ if (self.zoom) {
+ self.zoom.remove();
+ }
+ for (var a = 0; a < self.saved.css.length; a++) {
+ var d = self.saved.css[a];
+ d[0].css(d[1], (typeof d[2] == "undefined") ? '' : d[2]);
+ }
+ self.saved = false;
+ self.me.data('__nicescroll', ''); //erase all traces
+
+ // memory leak fixed by GianlucaGuarini - thanks a lot!
+ // remove the current nicescroll from the $.nicescroll array & normalize array
+ var lst = $.nicescroll;
+ lst.each(function(i) {
+ if (!this) return;
+ if (this.id === self.id) {
+ delete lst[i];
+ for (var b = ++i; b < lst.length; b++, i++) lst[i] = lst[b];
+ lst.length--;
+ if (lst.length) delete lst[lst.length];
+ }
+ });
+
+ for (var i in self) {
+ self[i] = null;
+ delete self[i];
+ }
+
+ self = null;
+
+ };
+
+ this.scrollstart = function(fn) {
+ this.onscrollstart = fn;
+ return self;
+ };
+ this.scrollend = function(fn) {
+ this.onscrollend = fn;
+ return self;
+ };
+ this.scrollcancel = function(fn) {
+ this.onscrollcancel = fn;
+ return self;
+ };
+
+ this.zoomin = function(fn) {
+ this.onzoomin = fn;
+ return self;
+ };
+ this.zoomout = function(fn) {
+ this.onzoomout = fn;
+ return self;
+ };
+
+ this.isScrollable = function(e) {
+ var dom = (e.target) ? e.target : e;
+ if (dom.nodeName == 'OPTION') return true;
+ while (dom && (dom.nodeType == 1) && !(/^BODY|HTML/.test(dom.nodeName))) {
+ var dd = $(dom);
+ var ov = dd.css('overflowY') || dd.css('overflowX') || dd.css('overflow') || '';
+ if (/scroll|auto/.test(ov)) return (dom.clientHeight != dom.scrollHeight);
+ dom = (dom.parentNode) ? dom.parentNode : false;
+ }
+ return false;
+ };
+
+ this.getViewport = function(me) {
+ var dom = (me && me.parentNode) ? me.parentNode : false;
+ while (dom && (dom.nodeType == 1) && !(/^BODY|HTML/.test(dom.nodeName))) {
+ var dd = $(dom);
+ if (/fixed|absolute/.test(dd.css("position"))) return dd;
+ var ov = dd.css('overflowY') || dd.css('overflowX') || dd.css('overflow') || '';
+ if ((/scroll|auto/.test(ov)) && (dom.clientHeight != dom.scrollHeight)) return dd;
+ if (dd.getNiceScroll().length > 0) return dd;
+ dom = (dom.parentNode) ? dom.parentNode : false;
+ }
+ return false; //(dom) ? $(dom) : false;
+ };
+
+ this.triggerScrollEnd = function() {
+ if (!self.onscrollend) return;
+
+ var px = self.getScrollLeft();
+ var py = self.getScrollTop();
+
+ var info = {
+ "type": "scrollend",
+ "current": {
+ "x": px,
+ "y": py
+ },
+ "end": {
+ "x": px,
+ "y": py
+ }
+ };
+ self.onscrollend.call(self, info);
+ }
+
+ function execScrollWheel(e, hr, chkscroll) {
+ var px, py;
+
+ if (e.deltaMode == 0) { // PIXEL
+ px = -Math.floor(e.deltaX * (self.opt.mousescrollstep / (18 * 3)));
+ py = -Math.floor(e.deltaY * (self.opt.mousescrollstep / (18 * 3)));
+ } else if (e.deltaMode == 1) { // LINE
+ px = -Math.floor(e.deltaX * self.opt.mousescrollstep);
+ py = -Math.floor(e.deltaY * self.opt.mousescrollstep);
+ }
+
+ if (hr && self.opt.oneaxismousemode && (px == 0) && py) { // classic vertical-only mousewheel + browser with x/y support
+ px = py;
+ py = 0;
+
+ if (chkscroll) {
+ var hrend = (px < 0) ? (self.getScrollLeft() >= self.page.maxw) : (self.getScrollLeft() <= 0);
+ if (hrend) { // preserve vertical scrolling
+ py = px;
+ px = 0;
+ }
+ }
+
+ }
+
+ if (px) {
+ if (self.scrollmom) {
+ self.scrollmom.stop()
+ }
+ self.lastdeltax += px;
+ self.debounced("mousewheelx", function() {
+ var dt = self.lastdeltax;
+ self.lastdeltax = 0;
+ if (!self.rail.drag) {
+ self.doScrollLeftBy(dt)
+ }
+ }, 15);
+ }
+ if (py) {
+ if (self.opt.nativeparentscrolling && chkscroll && !self.ispage && !self.zoomactive) {
+ if (py < 0) {
+ if (self.getScrollTop() >= self.page.maxh) return true;
+ } else {
+ if (self.getScrollTop() <= 0) return true;
+ }
+ }
+ if (self.scrollmom) {
+ self.scrollmom.stop()
+ }
+ self.lastdeltay += py;
+ self.debounced("mousewheely", function() {
+ var dt = self.lastdeltay;
+ self.lastdeltay = 0;
+ if (!self.rail.drag) {
+ self.doScrollBy(dt)
+ }
+ }, 15);
+ }
+
+ e.stopImmediatePropagation();
+ return e.preventDefault();
+ };
+
+ this.onmousewheel = function(e) {
+ if (self.wheelprevented) return;
+ if (self.railslocked) {
+ self.debounced("checkunlock", self.resize, 250);
+ return true;
+ }
+ if (self.rail.drag) return self.cancelEvent(e);
+
+ if (self.opt.oneaxismousemode == "auto" && e.deltaX != 0) self.opt.oneaxismousemode = false; // check two-axis mouse support (not very elegant)
+
+ if (self.opt.oneaxismousemode && e.deltaX == 0) {
+ if (!self.rail.scrollable) {
+ if (self.railh && self.railh.scrollable) {
+ return self.onmousewheelhr(e);
+ } else {
+ return true;
+ }
+ }
+ }
+
+ var nw = +(new Date());
+ var chk = false;
+ if (self.opt.preservenativescrolling && ((self.checkarea + 600) < nw)) {
+ self.nativescrollingarea = self.isScrollable(e);
+ chk = true;
+ }
+ self.checkarea = nw;
+ if (self.nativescrollingarea) return true; // this isn't my business
+ var ret = execScrollWheel(e, false, chk);
+ if (ret) self.checkarea = 0;
+ return ret;
+ };
+
+ this.onmousewheelhr = function(e) {
+ if (self.wheelprevented) return;
+ if (self.railslocked || !self.railh.scrollable) return true;
+ if (self.rail.drag) return self.cancelEvent(e);
+
+ var nw = +(new Date());
+ var chk = false;
+ if (self.opt.preservenativescrolling && ((self.checkarea + 600) < nw)) {
+ self.nativescrollingarea = self.isScrollable(e);
+ chk = true;
+ }
+ self.checkarea = nw;
+ if (self.nativescrollingarea) return true; // this isn't my business
+ if (self.railslocked) return self.cancelEvent(e);
+
+ return execScrollWheel(e, true, chk);
+ };
+
+ this.stop = function() {
+ self.cancelScroll();
+ if (self.scrollmon) self.scrollmon.stop();
+ self.cursorfreezed = false;
+ self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y));
+ self.noticeCursor();
+ return self;
+ };
+
+ this.getTransitionSpeed = function(dif) {
+ var sp = Math.round(self.opt.scrollspeed * 10);
+ var ex = Math.min(sp, Math.round((dif / 20) * self.opt.scrollspeed));
+ return (ex > 20) ? ex : 0;
+ };
+
+ if (!self.opt.smoothscroll) {
+ this.doScrollLeft = function(x, spd) { //direct
+ var y = self.getScrollTop();
+ self.doScrollPos(x, y, spd);
+ };
+ this.doScrollTop = function(y, spd) { //direct
+ var x = self.getScrollLeft();
+ self.doScrollPos(x, y, spd);
+ };
+ this.doScrollPos = function(x, y, spd) { //direct
+ var nx = (x > self.page.maxw) ? self.page.maxw : x;
+ if (nx < 0) nx = 0;
+ var ny = (y > self.page.maxh) ? self.page.maxh : y;
+ if (ny < 0) ny = 0;
+ self.synched('scroll', function() {
+ self.setScrollTop(ny);
+ self.setScrollLeft(nx);
+ });
+ };
+ this.cancelScroll = function() {}; // direct
+ } else if (self.ishwscroll && cap.hastransition && self.opt.usetransition && !!self.opt.smoothscroll) {
+ this.prepareTransition = function(dif, istime) {
+ var ex = (istime) ? ((dif > 20) ? dif : 0) : self.getTransitionSpeed(dif);
+ var trans = (ex) ? cap.prefixstyle + 'transform ' + ex + 'ms ease-out' : '';
+ if (!self.lasttransitionstyle || self.lasttransitionstyle != trans) {
+ self.lasttransitionstyle = trans;
+ self.doc.css(cap.transitionstyle, trans);
+ }
+ return ex;
+ };
+
+ this.doScrollLeft = function(x, spd) { //trans
+ var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop();
+ self.doScrollPos(x, y, spd);
+ };
+
+ this.doScrollTop = function(y, spd) { //trans
+ var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft();
+ self.doScrollPos(x, y, spd);
+ };
+
+ this.doScrollPos = function(x, y, spd) { //trans
+
+ var py = self.getScrollTop();
+ var px = self.getScrollLeft();
+
+ if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection
+
+ if (self.opt.bouncescroll == false) {
+ if (y < 0) y = 0;
+ else if (y > self.page.maxh) y = self.page.maxh;
+ if (x < 0) x = 0;
+ else if (x > self.page.maxw) x = self.page.maxw;
+ }
+
+ if (self.scrollrunning && x == self.newscrollx && y == self.newscrolly) return false;
+
+ self.newscrolly = y;
+ self.newscrollx = x;
+
+ self.newscrollspeed = spd || false;
+
+ if (self.timer) return false;
+
+ self.timer = setTimeout(function() {
+
+ var top = self.getScrollTop();
+ var lft = self.getScrollLeft();
+
+ var dst = {};
+ dst.x = x - lft;
+ dst.y = y - top;
+ dst.px = lft;
+ dst.py = top;
+
+ var dd = Math.round(Math.sqrt(Math.pow(dst.x, 2) + Math.pow(dst.y, 2)));
+ var ms = (self.newscrollspeed && self.newscrollspeed > 1) ? self.newscrollspeed : self.getTransitionSpeed(dd);
+ if (self.newscrollspeed && self.newscrollspeed <= 1) ms *= self.newscrollspeed;
+
+ self.prepareTransition(ms, true);
+
+ if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm);
+
+ if (ms > 0) {
+
+ if (!self.scrollrunning && self.onscrollstart) {
+ var info = {
+ "type": "scrollstart",
+ "current": {
+ "x": lft,
+ "y": top
+ },
+ "request": {
+ "x": x,
+ "y": y
+ },
+ "end": {
+ "x": self.newscrollx,
+ "y": self.newscrolly
+ },
+ "speed": ms
+ };
+ self.onscrollstart.call(self, info);
+ }
+
+ if (cap.transitionend) {
+ if (!self.scrollendtrapped) {
+ self.scrollendtrapped = true;
+ self.bind(self.doc, cap.transitionend, self.onScrollTransitionEnd, false); //I have got to do something usefull!!
+ }
+ } else {
+ if (self.scrollendtrapped) clearTimeout(self.scrollendtrapped);
+ self.scrollendtrapped = setTimeout(self.onScrollTransitionEnd, ms); // simulate transitionend event
+ }
+
+ var py = top;
+ var px = lft;
+ self.timerscroll = {
+ bz: new BezierClass(py, self.newscrolly, ms, 0, 0, 0.58, 1),
+ bh: new BezierClass(px, self.newscrollx, ms, 0, 0, 0.58, 1)
+ };
+ if (!self.cursorfreezed) self.timerscroll.tm = setInterval(function() {
+ self.showCursor(self.getScrollTop(), self.getScrollLeft())
+ }, 60);
+
+ }
+
+ self.synched("doScroll-set", function() {
+ self.timer = 0;
+ if (self.scrollendtrapped) self.scrollrunning = true;
+ self.setScrollTop(self.newscrolly);
+ self.setScrollLeft(self.newscrollx);
+ if (!self.scrollendtrapped) self.onScrollTransitionEnd();
+ });
+
+
+ }, 50);
+
+ };
+
+ this.cancelScroll = function() {
+ if (!self.scrollendtrapped) return true;
+ var py = self.getScrollTop();
+ var px = self.getScrollLeft();
+ self.scrollrunning = false;
+ if (!cap.transitionend) clearTimeout(cap.transitionend);
+ self.scrollendtrapped = false;
+ self._unbind(self.doc[0], cap.transitionend, self.onScrollTransitionEnd);
+ self.prepareTransition(0);
+ self.setScrollTop(py); // fire event onscroll
+ if (self.railh) self.setScrollLeft(px);
+ if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm);
+ self.timerscroll = false;
+
+ self.cursorfreezed = false;
+
+ self.showCursor(py, px);
+ return self;
+ };
+ this.onScrollTransitionEnd = function() {
+ if (self.scrollendtrapped) self._unbind(self.doc[0], cap.transitionend, self.onScrollTransitionEnd);
+ self.scrollendtrapped = false;
+ self.prepareTransition(0);
+ if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm);
+ self.timerscroll = false;
+ var py = self.getScrollTop();
+ var px = self.getScrollLeft();
+ self.setScrollTop(py); // fire event onscroll
+ if (self.railh) self.setScrollLeft(px); // fire event onscroll left
+
+ self.noticeCursor(false, py, px);
+
+ self.cursorfreezed = false;
+
+ if (py < 0) py = 0
+ else if (py > self.page.maxh) py = self.page.maxh;
+ if (px < 0) px = 0
+ else if (px > self.page.maxw) px = self.page.maxw;
+ if ((py != self.newscrolly) || (px != self.newscrollx)) return self.doScrollPos(px, py, self.opt.snapbackspeed);
+
+ if (self.onscrollend && self.scrollrunning) {
+ self.triggerScrollEnd();
+ }
+ self.scrollrunning = false;
+
+ };
+
+ } else {
+
+ this.doScrollLeft = function(x, spd) { //no-trans
+ var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop();
+ self.doScrollPos(x, y, spd);
+ };
+
+ this.doScrollTop = function(y, spd) { //no-trans
+ var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft();
+ self.doScrollPos(x, y, spd);
+ };
+
+ this.doScrollPos = function(x, y, spd) { //no-trans
+ var y = ((typeof y == "undefined") || (y === false)) ? self.getScrollTop(true) : y;
+
+ if ((self.timer) && (self.newscrolly == y) && (self.newscrollx == x)) return true;
+
+ if (self.timer) clearAnimationFrame(self.timer);
+ self.timer = 0;
+
+ var py = self.getScrollTop();
+ var px = self.getScrollLeft();
+
+ if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection
+
+ self.newscrolly = y;
+ self.newscrollx = x;
+
+ if (!self.bouncescroll || !self.rail.visibility) {
+ if (self.newscrolly < 0) {
+ self.newscrolly = 0;
+ } else if (self.newscrolly > self.page.maxh) {
+ self.newscrolly = self.page.maxh;
+ }
+ }
+ if (!self.bouncescroll || !self.railh.visibility) {
+ if (self.newscrollx < 0) {
+ self.newscrollx = 0;
+ } else if (self.newscrollx > self.page.maxw) {
+ self.newscrollx = self.page.maxw;
+ }
+ }
+
+ self.dst = {};
+ self.dst.x = x - px;
+ self.dst.y = y - py;
+ self.dst.px = px;
+ self.dst.py = py;
+
+ var dst = Math.round(Math.sqrt(Math.pow(self.dst.x, 2) + Math.pow(self.dst.y, 2)));
+
+ self.dst.ax = self.dst.x / dst;
+ self.dst.ay = self.dst.y / dst;
+
+ var pa = 0;
+ var pe = dst;
+
+ if (self.dst.x == 0) {
+ pa = py;
+ pe = y;
+ self.dst.ay = 1;
+ self.dst.py = 0;
+ } else if (self.dst.y == 0) {
+ pa = px;
+ pe = x;
+ self.dst.ax = 1;
+ self.dst.px = 0;
+ }
+
+ var ms = self.getTransitionSpeed(dst);
+ if (spd && spd <= 1) ms *= spd;
+ if (ms > 0) {
+ self.bzscroll = (self.bzscroll) ? self.bzscroll.update(pe, ms) : new BezierClass(pa, pe, ms, 0, 1, 0, 1);
+ } else {
+ self.bzscroll = false;
+ }
+
+ if (self.timer) return;
+
+ if ((py == self.page.maxh && y >= self.page.maxh) || (px == self.page.maxw && x >= self.page.maxw)) self.checkContentSize();
+
+ var sync = 1;
+
+ function scrolling() {
+ if (self.cancelAnimationFrame) return true;
+
+ self.scrollrunning = true;
+
+ sync = 1 - sync;
+ if (sync) return (self.timer = setAnimationFrame(scrolling) || 1);
+
+ var done = 0;
+ var sx, sy;
+
+ var sc = sy = self.getScrollTop();
+ if (self.dst.ay) {
+ sc = (self.bzscroll) ? self.dst.py + (self.bzscroll.getNow() * self.dst.ay) : self.newscrolly;
+ var dr = sc - sy;
+ if ((dr < 0 && sc < self.newscrolly) || (dr > 0 && sc > self.newscrolly)) sc = self.newscrolly;
+ self.setScrollTop(sc);
+ if (sc == self.newscrolly) done = 1;
+ } else {
+ done = 1;
+ }
+
+ var scx = sx = self.getScrollLeft();
+ if (self.dst.ax) {
+ scx = (self.bzscroll) ? self.dst.px + (self.bzscroll.getNow() * self.dst.ax) : self.newscrollx;
+ var dr = scx - sx;
+ if ((dr < 0 && scx < self.newscrollx) || (dr > 0 && scx > self.newscrollx)) scx = self.newscrollx;
+ self.setScrollLeft(scx);
+ if (scx == self.newscrollx) done += 1;
+ } else {
+ done += 1;
+ }
+
+ if (done == 2) {
+ self.timer = 0;
+ self.cursorfreezed = false;
+ self.bzscroll = false;
+ self.scrollrunning = false;
+ if (sc < 0) sc = 0;
+ else if (sc > self.page.maxh) sc = self.page.maxh;
+ if (scx < 0) scx = 0;
+ else if (scx > self.page.maxw) scx = self.page.maxw;
+ if ((scx != self.newscrollx) || (sc != self.newscrolly)) self.doScrollPos(scx, sc);
+ else {
+ if (self.onscrollend) {
+ self.triggerScrollEnd();
+ }
+ }
+ } else {
+ self.timer = setAnimationFrame(scrolling) || 1;
+ }
+ };
+ self.cancelAnimationFrame = false;
+ self.timer = 1;
+
+ if (self.onscrollstart && !self.scrollrunning) {
+ var info = {
+ "type": "scrollstart",
+ "current": {
+ "x": px,
+ "y": py
+ },
+ "request": {
+ "x": x,
+ "y": y
+ },
+ "end": {
+ "x": self.newscrollx,
+ "y": self.newscrolly
+ },
+ "speed": ms
+ };
+ self.onscrollstart.call(self, info);
+ }
+
+ scrolling();
+
+ if ((py == self.page.maxh && y >= py) || (px == self.page.maxw && x >= px)) self.checkContentSize();
+
+ self.noticeCursor();
+ };
+
+ this.cancelScroll = function() {
+ if (self.timer) clearAnimationFrame(self.timer);
+ self.timer = 0;
+ self.bzscroll = false;
+ self.scrollrunning = false;
+ return self;
+ };
+
+ }
+
+ this.doScrollBy = function(stp, relative) {
+ var ny = 0;
+ if (relative) {
+ ny = Math.floor((self.scroll.y - stp) * self.scrollratio.y)
+ } else {
+ var sy = (self.timer) ? self.newscrolly : self.getScrollTop(true);
+ ny = sy - stp;
+ }
+ if (self.bouncescroll) {
+ var haf = Math.round(self.view.h / 2);
+ if (ny < -haf) ny = -haf
+ else if (ny > (self.page.maxh + haf)) ny = (self.page.maxh + haf);
+ }
+ self.cursorfreezed = false;
+
+ var py = self.getScrollTop(true);
+ if (ny < 0 && py <= 0) return self.noticeCursor();
+ else if (ny > self.page.maxh && py >= self.page.maxh) {
+ self.checkContentSize();
+ return self.noticeCursor();
+ }
+
+ self.doScrollTop(ny);
+ };
+
+ this.doScrollLeftBy = function(stp, relative) {
+ var nx = 0;
+ if (relative) {
+ nx = Math.floor((self.scroll.x - stp) * self.scrollratio.x)
+ } else {
+ var sx = (self.timer) ? self.newscrollx : self.getScrollLeft(true);
+ nx = sx - stp;
+ }
+ if (self.bouncescroll) {
+ var haf = Math.round(self.view.w / 2);
+ if (nx < -haf) nx = -haf;
+ else if (nx > (self.page.maxw + haf)) nx = (self.page.maxw + haf);
+ }
+ self.cursorfreezed = false;
+
+ var px = self.getScrollLeft(true);
+ if (nx < 0 && px <= 0) return self.noticeCursor();
+ else if (nx > self.page.maxw && px >= self.page.maxw) return self.noticeCursor();
+
+ self.doScrollLeft(nx);
+ };
+
+ this.doScrollTo = function(pos, relative) {
+ var ny = (relative) ? Math.round(pos * self.scrollratio.y) : pos;
+ if (ny < 0) ny = 0;
+ else if (ny > self.page.maxh) ny = self.page.maxh;
+ self.cursorfreezed = false;
+ self.doScrollTop(pos);
+ };
+
+ this.checkContentSize = function() {
+ var pg = self.getContentSize();
+ if ((pg.h != self.page.h) || (pg.w != self.page.w)) self.resize(false, pg);
+ };
+
+ self.onscroll = function(e) {
+ if (self.rail.drag) return;
+ if (!self.cursorfreezed) {
+ self.synched('scroll', function() {
+ self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y));
+ if (self.railh) self.scroll.x = Math.round(self.getScrollLeft() * (1 / self.scrollratio.x));
+ self.noticeCursor();
+ });
+ }
+ };
+ self.bind(self.docscroll, "scroll", self.onscroll);
+
+ this.doZoomIn = function(e) {
+ if (self.zoomactive) return;
+ self.zoomactive = true;
+
+ self.zoomrestore = {
+ style: {}
+ };
+ var lst = ['position', 'top', 'left', 'zIndex', 'backgroundColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight'];
+ var win = self.win[0].style;
+ for (var a in lst) {
+ var pp = lst[a];
+ self.zoomrestore.style[pp] = (typeof win[pp] != "undefined") ? win[pp] : '';
+ }
+
+ self.zoomrestore.style.width = self.win.css('width');
+ self.zoomrestore.style.height = self.win.css('height');
+
+ self.zoomrestore.padding = {
+ w: self.win.outerWidth() - self.win.width(),
+ h: self.win.outerHeight() - self.win.height()
+ };
+
+ if (cap.isios4) {
+ self.zoomrestore.scrollTop = $(window).scrollTop();
+ $(window).scrollTop(0);
+ }
+
+ self.win.css({
+ "position": (cap.isios4) ? "absolute" : "fixed",
+ "top": 0,
+ "left": 0,
+ "z-index": globalmaxzindex + 100,
+ "margin": "0px"
+ });
+ var bkg = self.win.css("backgroundColor");
+ if (bkg == "" || /transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(bkg)) self.win.css("backgroundColor", "#fff");
+ self.rail.css({
+ "z-index": globalmaxzindex + 101
+ });
+ self.zoom.css({
+ "z-index": globalmaxzindex + 102
+ });
+ self.zoom.css('backgroundPosition', '0px -18px');
+ self.resizeZoom();
+
+ if (self.onzoomin) self.onzoomin.call(self);
+
+ return self.cancelEvent(e);
+ };
+
+ this.doZoomOut = function(e) {
+ if (!self.zoomactive) return;
+ self.zoomactive = false;
+
+ self.win.css("margin", "");
+ self.win.css(self.zoomrestore.style);
+
+ if (cap.isios4) {
+ $(window).scrollTop(self.zoomrestore.scrollTop);
+ }
+
+ self.rail.css({
+ "z-index": self.zindex
+ });
+ self.zoom.css({
+ "z-index": self.zindex
+ });
+ self.zoomrestore = false;
+ self.zoom.css('backgroundPosition', '0px 0px');
+ self.onResize();
+
+ if (self.onzoomout) self.onzoomout.call(self);
+
+ return self.cancelEvent(e);
+ };
+
+ this.doZoom = function(e) {
+ return (self.zoomactive) ? self.doZoomOut(e) : self.doZoomIn(e);
+ };
+
+ this.resizeZoom = function() {
+ if (!self.zoomactive) return;
+
+ var py = self.getScrollTop(); //preserve scrolling position
+ self.win.css({
+ width: $(window).width() - self.zoomrestore.padding.w + "px",
+ height: $(window).height() - self.zoomrestore.padding.h + "px"
+ });
+ self.onResize();
+
+ self.setScrollTop(Math.min(self.page.maxh, py));
+ };
+
+ this.init();
+
+ $.nicescroll.push(this);
+
+ };
+
+ // Inspired by the work of Kin Blas
+ // http://webpro.host.adobe.com/people/jblas/momentum/includes/jquery.momentum.0.7.js
+
+
+ var ScrollMomentumClass2D = function(nc) {
+ var self = this;
+ this.nc = nc;
+
+ this.lastx = 0;
+ this.lasty = 0;
+ this.speedx = 0;
+ this.speedy = 0;
+ this.lasttime = 0;
+ this.steptime = 0;
+ this.snapx = false;
+ this.snapy = false;
+ this.demulx = 0;
+ this.demuly = 0;
+
+ this.lastscrollx = -1;
+ this.lastscrolly = -1;
+
+ this.chkx = 0;
+ this.chky = 0;
+
+ this.timer = 0;
+
+ this.time = function() {
+ return +new Date(); //beautifull hack
+ };
+
+ this.reset = function(px, py) {
+ self.stop();
+ var now = self.time();
+ self.steptime = 0;
+ self.lasttime = now;
+ self.speedx = 0;
+ self.speedy = 0;
+ self.lastx = px;
+ self.lasty = py;
+ self.lastscrollx = -1;
+ self.lastscrolly = -1;
+ };
+
+ this.update = function(px, py) {
+ var now = self.time();
+ self.steptime = now - self.lasttime;
+ self.lasttime = now;
+ var dy = py - self.lasty;
+ var dx = px - self.lastx;
+ var sy = self.nc.getScrollTop();
+ var sx = self.nc.getScrollLeft();
+ var newy = sy + dy;
+ var newx = sx + dx;
+ self.snapx = (newx < 0) || (newx > self.nc.page.maxw);
+ self.snapy = (newy < 0) || (newy > self.nc.page.maxh);
+ self.speedx = dx;
+ self.speedy = dy;
+ self.lastx = px;
+ self.lasty = py;
+ };
+
+ this.stop = function() {
+ self.nc.unsynched("domomentum2d");
+ if (self.timer) clearTimeout(self.timer);
+ self.timer = 0;
+ self.lastscrollx = -1;
+ self.lastscrolly = -1;
+ };
+
+ this.doSnapy = function(nx, ny) {
+ var snap = false;
+
+ if (ny < 0) {
+ ny = 0;
+ snap = true;
+ } else if (ny > self.nc.page.maxh) {
+ ny = self.nc.page.maxh;
+ snap = true;
+ }
+
+ if (nx < 0) {
+ nx = 0;
+ snap = true;
+ } else if (nx > self.nc.page.maxw) {
+ nx = self.nc.page.maxw;
+ snap = true;
+ }
+
+ (snap) ? self.nc.doScrollPos(nx, ny, self.nc.opt.snapbackspeed): self.nc.triggerScrollEnd();
+ };
+
+ this.doMomentum = function(gp) {
+ var t = self.time();
+ var l = (gp) ? t + gp : self.lasttime;
+
+ var sl = self.nc.getScrollLeft();
+ var st = self.nc.getScrollTop();
+
+ var pageh = self.nc.page.maxh;
+ var pagew = self.nc.page.maxw;
+
+ self.speedx = (pagew > 0) ? Math.min(60, self.speedx) : 0;
+ self.speedy = (pageh > 0) ? Math.min(60, self.speedy) : 0;
+
+ var chk = l && (t - l) <= 60;
+
+ if ((st < 0) || (st > pageh) || (sl < 0) || (sl > pagew)) chk = false;
+
+ var sy = (self.speedy && chk) ? self.speedy : false;
+ var sx = (self.speedx && chk) ? self.speedx : false;
+
+ if (sy || sx) {
+ var tm = Math.max(16, self.steptime); //timeout granularity
+
+ if (tm > 50) { // do smooth
+ var xm = tm / 50;
+ self.speedx *= xm;
+ self.speedy *= xm;
+ tm = 50;
+ }
+
+ self.demulxy = 0;
+
+ self.lastscrollx = self.nc.getScrollLeft();
+ self.chkx = self.lastscrollx;
+ self.lastscrolly = self.nc.getScrollTop();
+ self.chky = self.lastscrolly;
+
+ var nx = self.lastscrollx;
+ var ny = self.lastscrolly;
+
+ var onscroll = function() {
+ var df = ((self.time() - t) > 600) ? 0.04 : 0.02;
+
+ if (self.speedx) {
+ nx = Math.floor(self.lastscrollx - (self.speedx * (1 - self.demulxy)));
+ self.lastscrollx = nx;
+ if ((nx < 0) || (nx > pagew)) df = 0.10;
+ }
+
+ if (self.speedy) {
+ ny = Math.floor(self.lastscrolly - (self.speedy * (1 - self.demulxy)));
+ self.lastscrolly = ny;
+ if ((ny < 0) || (ny > pageh)) df = 0.10;
+ }
+
+ self.demulxy = Math.min(1, self.demulxy + df);
+
+ self.nc.synched("domomentum2d", function() {
+
+ if (self.speedx) {
+ var scx = self.nc.getScrollLeft();
+ if (scx != self.chkx) self.stop();
+ self.chkx = nx;
+ self.nc.setScrollLeft(nx);
+ }
+
+ if (self.speedy) {
+ var scy = self.nc.getScrollTop();
+ if (scy != self.chky) self.stop();
+ self.chky = ny;
+ self.nc.setScrollTop(ny);
+ }
+
+ if (!self.timer) {
+ self.nc.hideCursor();
+ self.doSnapy(nx, ny);
+ }
+
+ });
+
+ if (self.demulxy < 1) {
+ self.timer = setTimeout(onscroll, tm);
+ } else {
+ self.stop();
+ self.nc.hideCursor();
+ self.doSnapy(nx, ny);
+ }
+ };
+
+ onscroll();
+
+ } else {
+ self.doSnapy(self.nc.getScrollLeft(), self.nc.getScrollTop());
+ }
+
+ }
+
+ };
+
+
+ // override jQuery scrollTop
+
+ var _scrollTop = jQuery.fn.scrollTop; // preserve original function
+
+ jQuery.cssHooks["pageYOffset"] = {
+ get: function(elem, computed, extra) {
+ var nice = $.data(elem, '__nicescroll') || false;
+ return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(elem);
+ },
+ set: function(elem, value) {
+ var nice = $.data(elem, '__nicescroll') || false;
+ (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)): _scrollTop.call(elem, value);
+ return this;
+ }
+ };
+
+ /*
+ $.fx.step["scrollTop"] = function(fx){
+ $.cssHooks["scrollTop"].set( fx.elem, fx.now + fx.unit );
+ };
+*/
+
+ jQuery.fn.scrollTop = function(value) {
+ if (typeof value == "undefined") {
+ var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false;
+ return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(this);
+ } else {
+ return this.each(function() {
+ var nice = $.data(this, '__nicescroll') || false;
+ (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)): _scrollTop.call($(this), value);
+ });
+ }
+ };
+
+ // override jQuery scrollLeft
+
+ var _scrollLeft = jQuery.fn.scrollLeft; // preserve original function
+
+ $.cssHooks.pageXOffset = {
+ get: function(elem, computed, extra) {
+ var nice = $.data(elem, '__nicescroll') || false;
+ return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(elem);
+ },
+ set: function(elem, value) {
+ var nice = $.data(elem, '__nicescroll') || false;
+ (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)): _scrollLeft.call(elem, value);
+ return this;
+ }
+ };
+
+ /*
+ $.fx.step["scrollLeft"] = function(fx){
+ $.cssHooks["scrollLeft"].set( fx.elem, fx.now + fx.unit );
+ };
+*/
+
+ jQuery.fn.scrollLeft = function(value) {
+ if (typeof value == "undefined") {
+ var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false;
+ return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(this);
+ } else {
+ return this.each(function() {
+ var nice = $.data(this, '__nicescroll') || false;
+ (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)): _scrollLeft.call($(this), value);
+ });
+ }
+ };
+
+ var NiceScrollArray = function(doms) {
+ var self = this;
+ this.length = 0;
+ this.name = "nicescrollarray";
+
+ this.each = function(fn) {
+ for (var a = 0, i = 0; a < self.length; a++) fn.call(self[a], i++);
+ return self;
+ };
+
+ this.push = function(nice) {
+ self[self.length] = nice;
+ self.length++;
+ };
+
+ this.eq = function(idx) {
+ return self[idx];
+ };
+
+ if (doms) {
+ for (var a = 0; a < doms.length; a++) {
+ var nice = $.data(doms[a], '__nicescroll') || false;
+ if (nice) {
+ this[this.length] = nice;
+ this.length++;
+ }
+ };
+ }
+
+ return this;
+ };
+
+ function mplex(el, lst, fn) {
+ for (var a = 0; a < lst.length; a++) fn(el, lst[a]);
+ };
+ mplex(
+ NiceScrollArray.prototype, ['show', 'hide', 'toggle', 'onResize', 'resize', 'remove', 'stop', 'doScrollPos'],
+ function(e, n) {
+ e[n] = function() {
+ var args = arguments;
+ return this.each(function() {
+ this[n].apply(this, args);
+ });
+ };
+ }
+ );
+
+ jQuery.fn.getNiceScroll = function(index) {
+ if (typeof index == "undefined") {
+ return new NiceScrollArray(this);
+ } else {
+ var nice = this[index] && $.data(this[index], '__nicescroll') || false;
+ return nice;
+ }
+ };
+
+ jQuery.extend(jQuery.expr[':'], {
+ nicescroll: function(a) {
+ return ($.data(a, '__nicescroll')) ? true : false;
+ }
+ });
+
+ $.fn.niceScroll = function(wrapper, opt) {
+ if (typeof opt == "undefined") {
+ if ((typeof wrapper == "object") && !("jquery" in wrapper)) {
+ opt = wrapper;
+ wrapper = false;
+ }
+ }
+ opt = $.extend({},opt); // cloning
+ var ret = new NiceScrollArray();
+ if (typeof opt == "undefined") opt = {};
+
+ if (wrapper || false) {
+ opt.doc = $(wrapper);
+ opt.win = $(this);
+ }
+ var docundef = !("doc" in opt);
+ if (!docundef && !("win" in opt)) opt.win = $(this);
+
+ this.each(function() {
+ var nice = $(this).data('__nicescroll') || false;
+ if (!nice) {
+ opt.doc = (docundef) ? $(this) : opt.doc;
+ nice = new NiceScrollClass(opt, $(this));
+ $(this).data('__nicescroll', nice);
+ }
+ ret.push(nice);
+ });
+ return (ret.length == 1) ? ret[0] : ret;
+ };
+
+ window.NiceScroll = {
+ getjQuery: function() {
+ return jQuery
+ }
+ };
+
+ if (!$.nicescroll) {
+ $.nicescroll = new NiceScrollArray();
+ $.nicescroll.options = _globaloptions;
+ }
+
+})); \ No newline at end of file
diff --git a/vendor/assets/javascripts/jquery.nicescroll.min.js b/vendor/assets/javascripts/jquery.nicescroll.min.js
deleted file mode 100644
index 5440b6a0da0..00000000000
--- a/vendor/assets/javascripts/jquery.nicescroll.min.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/* jquery.nicescroll 3.6.0 InuYaksa*2014 MIT http://nicescroll.areaaperta.com */(function(f){"function"===typeof define&&define.amd?define(["jquery"],f):f(jQuery)})(function(f){var y=!1,D=!1,N=0,O=2E3,x=0,H=["webkit","ms","moz","o"],s=window.requestAnimationFrame||!1,t=window.cancelAnimationFrame||!1;if(!s)for(var P in H){var E=H[P];s||(s=window[E+"RequestAnimationFrame"]);t||(t=window[E+"CancelAnimationFrame"]||window[E+"CancelRequestAnimationFrame"])}var v=window.MutationObserver||window.WebKitMutationObserver||!1,I={zindex:"auto",cursoropacitymin:0,cursoropacitymax:1,cursorcolor:"#424242",
-cursorwidth:"5px",cursorborder:"1px solid #fff",cursorborderradius:"5px",scrollspeed:60,mousescrollstep:24,touchbehavior:!1,hwacceleration:!0,usetransition:!0,boxzoom:!1,dblclickzoom:!0,gesturezoom:!0,grabcursorenabled:!0,autohidemode:!0,background:"",iframeautoresize:!0,cursorminheight:32,preservenativescrolling:!0,railoffset:!1,railhoffset:!1,bouncescroll:!0,spacebarenabled:!0,railpadding:{top:0,right:0,left:0,bottom:0},disableoutline:!0,horizrailenabled:!0,railalign:"right",railvalign:"bottom",
-enabletranslate3d:!0,enablemousewheel:!0,enablekeyboard:!0,smoothscroll:!0,sensitiverail:!0,enablemouselockapi:!0,cursorfixedheight:!1,directionlockdeadzone:6,hidecursordelay:400,nativeparentscrolling:!0,enablescrollonselection:!0,overflowx:!0,overflowy:!0,cursordragspeed:.3,rtlmode:"auto",cursordragontouch:!1,oneaxismousemode:"auto",scriptpath:function(){var f=document.getElementsByTagName("script"),f=f[f.length-1].src.split("?")[0];return 0<f.split("/").length?f.split("/").slice(0,-1).join("/")+
-"/":""}(),preventmultitouchscrolling:!0},F=!1,Q=function(){if(F)return F;var f=document.createElement("DIV"),c=f.style,h=navigator.userAgent,m=navigator.platform,d={haspointerlock:"pointerLockElement"in document||"webkitPointerLockElement"in document||"mozPointerLockElement"in document};d.isopera="opera"in window;d.isopera12=d.isopera&&"getUserMedia"in navigator;d.isoperamini="[object OperaMini]"===Object.prototype.toString.call(window.operamini);d.isie="all"in document&&"attachEvent"in f&&!d.isopera;
-d.isieold=d.isie&&!("msInterpolationMode"in c);d.isie7=d.isie&&!d.isieold&&(!("documentMode"in document)||7==document.documentMode);d.isie8=d.isie&&"documentMode"in document&&8==document.documentMode;d.isie9=d.isie&&"performance"in window&&9<=document.documentMode;d.isie10=d.isie&&"performance"in window&&10==document.documentMode;d.isie11="msRequestFullscreen"in f&&11<=document.documentMode;d.isie9mobile=/iemobile.9/i.test(h);d.isie9mobile&&(d.isie9=!1);d.isie7mobile=!d.isie9mobile&&d.isie7&&/iemobile/i.test(h);
-d.ismozilla="MozAppearance"in c;d.iswebkit="WebkitAppearance"in c;d.ischrome="chrome"in window;d.ischrome22=d.ischrome&&d.haspointerlock;d.ischrome26=d.ischrome&&"transition"in c;d.cantouch="ontouchstart"in document.documentElement||"ontouchstart"in window;d.hasmstouch=window.MSPointerEvent||!1;d.hasw3ctouch=window.PointerEvent||!1;d.ismac=/^mac$/i.test(m);d.isios=d.cantouch&&/iphone|ipad|ipod/i.test(m);d.isios4=d.isios&&!("seal"in Object);d.isios7=d.isios&&"webkitHidden"in document;d.isandroid=/android/i.test(h);
-d.haseventlistener="addEventListener"in f;d.trstyle=!1;d.hastransform=!1;d.hastranslate3d=!1;d.transitionstyle=!1;d.hastransition=!1;d.transitionend=!1;m=["transform","msTransform","webkitTransform","MozTransform","OTransform"];for(h=0;h<m.length;h++)if("undefined"!=typeof c[m[h]]){d.trstyle=m[h];break}d.hastransform=!!d.trstyle;d.hastransform&&(c[d.trstyle]="translate3d(1px,2px,3px)",d.hastranslate3d=/translate3d/.test(c[d.trstyle]));d.transitionstyle=!1;d.prefixstyle="";d.transitionend=!1;for(var m=
-"transition webkitTransition msTransition MozTransition OTransition OTransition KhtmlTransition".split(" "),n=" -webkit- -ms- -moz- -o- -o -khtml-".split(" "),p="transitionend webkitTransitionEnd msTransitionEnd transitionend otransitionend oTransitionEnd KhtmlTransitionEnd".split(" "),h=0;h<m.length;h++)if(m[h]in c){d.transitionstyle=m[h];d.prefixstyle=n[h];d.transitionend=p[h];break}d.ischrome26&&(d.prefixstyle=n[1]);d.hastransition=d.transitionstyle;a:{h=["-webkit-grab","-moz-grab","grab"];if(d.ischrome&&
-!d.ischrome22||d.isie)h=[];for(m=0;m<h.length;m++)if(n=h[m],c.cursor=n,c.cursor==n){c=n;break a}c="url(//mail.google.com/mail/images/2/openhand.cur),n-resize"}d.cursorgrabvalue=c;d.hasmousecapture="setCapture"in f;d.hasMutationObserver=!1!==v;return F=d},R=function(k,c){function h(){var b=a.doc.css(e.trstyle);return b&&"matrix"==b.substr(0,6)?b.replace(/^.*\((.*)\)$/g,"$1").replace(/px/g,"").split(/, +/):!1}function m(){var b=a.win;if("zIndex"in b)return b.zIndex();for(;0<b.length&&9!=b[0].nodeType;){var g=
-b.css("zIndex");if(!isNaN(g)&&0!=g)return parseInt(g);b=b.parent()}return!1}function d(b,g,q){g=b.css(g);b=parseFloat(g);return isNaN(b)?(b=w[g]||0,q=3==b?q?a.win.outerHeight()-a.win.innerHeight():a.win.outerWidth()-a.win.innerWidth():1,a.isie8&&b&&(b+=1),q?b:0):b}function n(b,g,q,c){a._bind(b,g,function(a){a=a?a:window.event;var c={original:a,target:a.target||a.srcElement,type:"wheel",deltaMode:"MozMousePixelScroll"==a.type?0:1,deltaX:0,deltaZ:0,preventDefault:function(){a.preventDefault?a.preventDefault():
-a.returnValue=!1;return!1},stopImmediatePropagation:function(){a.stopImmediatePropagation?a.stopImmediatePropagation():a.cancelBubble=!0}};"mousewheel"==g?(c.deltaY=-.025*a.wheelDelta,a.wheelDeltaX&&(c.deltaX=-.025*a.wheelDeltaX)):c.deltaY=a.detail;return q.call(b,c)},c)}function p(b,g,c){var d,e;0==b.deltaMode?(d=-Math.floor(a.opt.mousescrollstep/54*b.deltaX),e=-Math.floor(a.opt.mousescrollstep/54*b.deltaY)):1==b.deltaMode&&(d=-Math.floor(b.deltaX*a.opt.mousescrollstep),e=-Math.floor(b.deltaY*a.opt.mousescrollstep));
-g&&a.opt.oneaxismousemode&&0==d&&e&&(d=e,e=0,c&&(0>d?a.getScrollLeft()>=a.page.maxw:0>=a.getScrollLeft())&&(e=d,d=0));d&&(a.scrollmom&&a.scrollmom.stop(),a.lastdeltax+=d,a.debounced("mousewheelx",function(){var b=a.lastdeltax;a.lastdeltax=0;a.rail.drag||a.doScrollLeftBy(b)},15));if(e){if(a.opt.nativeparentscrolling&&c&&!a.ispage&&!a.zoomactive)if(0>e){if(a.getScrollTop()>=a.page.maxh)return!0}else if(0>=a.getScrollTop())return!0;a.scrollmom&&a.scrollmom.stop();a.lastdeltay+=e;a.debounced("mousewheely",
-function(){var b=a.lastdeltay;a.lastdeltay=0;a.rail.drag||a.doScrollBy(b)},15)}b.stopImmediatePropagation();return b.preventDefault()}var a=this;this.version="3.6.0";this.name="nicescroll";this.me=c;this.opt={doc:f("body"),win:!1};f.extend(this.opt,I);this.opt.snapbackspeed=80;if(k)for(var G in a.opt)"undefined"!=typeof k[G]&&(a.opt[G]=k[G]);this.iddoc=(this.doc=a.opt.doc)&&this.doc[0]?this.doc[0].id||"":"";this.ispage=/^BODY|HTML/.test(a.opt.win?a.opt.win[0].nodeName:this.doc[0].nodeName);this.haswrapper=
-!1!==a.opt.win;this.win=a.opt.win||(this.ispage?f(window):this.doc);this.docscroll=this.ispage&&!this.haswrapper?f(window):this.win;this.body=f("body");this.iframe=this.isfixed=this.viewport=!1;this.isiframe="IFRAME"==this.doc[0].nodeName&&"IFRAME"==this.win[0].nodeName;this.istextarea="TEXTAREA"==this.win[0].nodeName;this.forcescreen=!1;this.canshowonmouseevent="scroll"!=a.opt.autohidemode;this.page=this.view=this.onzoomout=this.onzoomin=this.onscrollcancel=this.onscrollend=this.onscrollstart=this.onclick=
-this.ongesturezoom=this.onkeypress=this.onmousewheel=this.onmousemove=this.onmouseup=this.onmousedown=!1;this.scroll={x:0,y:0};this.scrollratio={x:0,y:0};this.cursorheight=20;this.scrollvaluemax=0;this.isrtlmode="auto"==this.opt.rtlmode?"rtl"==(this.win[0]==window?this.body:this.win).css("direction"):!0===this.opt.rtlmode;this.observerbody=this.observerremover=this.observer=this.scrollmom=this.scrollrunning=!1;do this.id="ascrail"+O++;while(document.getElementById(this.id));this.hasmousefocus=this.hasfocus=
-this.zoomactive=this.zoom=this.selectiondrag=this.cursorfreezed=this.cursor=this.rail=!1;this.visibility=!0;this.hidden=this.locked=this.railslocked=!1;this.cursoractive=!0;this.wheelprevented=!1;this.overflowx=a.opt.overflowx;this.overflowy=a.opt.overflowy;this.nativescrollingarea=!1;this.checkarea=0;this.events=[];this.saved={};this.delaylist={};this.synclist={};this.lastdeltay=this.lastdeltax=0;this.detected=Q();var e=f.extend({},this.detected);this.ishwscroll=(this.canhwscroll=e.hastransform&&
-a.opt.hwacceleration)&&a.haswrapper;this.hasreversehr=this.isrtlmode&&!e.iswebkit;this.istouchcapable=!1;!e.cantouch||e.isios||e.isandroid||!e.iswebkit&&!e.ismozilla||(this.istouchcapable=!0,e.cantouch=!1);a.opt.enablemouselockapi||(e.hasmousecapture=!1,e.haspointerlock=!1);this.debounced=function(b,g,c){var d=a.delaylist[b];a.delaylist[b]=g;d||setTimeout(function(){var g=a.delaylist[b];a.delaylist[b]=!1;g.call(a)},c)};var r=!1;this.synched=function(b,g){a.synclist[b]=g;(function(){r||(s(function(){r=
-!1;for(var b in a.synclist){var g=a.synclist[b];g&&g.call(a);a.synclist[b]=!1}}),r=!0)})();return b};this.unsynched=function(b){a.synclist[b]&&(a.synclist[b]=!1)};this.css=function(b,g){for(var c in g)a.saved.css.push([b,c,b.css(c)]),b.css(c,g[c])};this.scrollTop=function(b){return"undefined"==typeof b?a.getScrollTop():a.setScrollTop(b)};this.scrollLeft=function(b){return"undefined"==typeof b?a.getScrollLeft():a.setScrollLeft(b)};var A=function(a,g,c,d,e,f,h){this.st=a;this.ed=g;this.spd=c;this.p1=
-d||0;this.p2=e||1;this.p3=f||0;this.p4=h||1;this.ts=(new Date).getTime();this.df=this.ed-this.st};A.prototype={B2:function(a){return 3*a*a*(1-a)},B3:function(a){return 3*a*(1-a)*(1-a)},B4:function(a){return(1-a)*(1-a)*(1-a)},getNow:function(){var a=1-((new Date).getTime()-this.ts)/this.spd,g=this.B2(a)+this.B3(a)+this.B4(a);return 0>a?this.ed:this.st+Math.round(this.df*g)},update:function(a,g){this.st=this.getNow();this.ed=a;this.spd=g;this.ts=(new Date).getTime();this.df=this.ed-this.st;return this}};
-if(this.ishwscroll){this.doc.translate={x:0,y:0,tx:"0px",ty:"0px"};e.hastranslate3d&&e.isios&&this.doc.css("-webkit-backface-visibility","hidden");this.getScrollTop=function(b){if(!b){if(b=h())return 16==b.length?-b[13]:-b[5];if(a.timerscroll&&a.timerscroll.bz)return a.timerscroll.bz.getNow()}return a.doc.translate.y};this.getScrollLeft=function(b){if(!b){if(b=h())return 16==b.length?-b[12]:-b[4];if(a.timerscroll&&a.timerscroll.bh)return a.timerscroll.bh.getNow()}return a.doc.translate.x};this.notifyScrollEvent=
-function(a){var g=document.createEvent("UIEvents");g.initUIEvent("scroll",!1,!0,window,1);g.niceevent=!0;a.dispatchEvent(g)};var K=this.isrtlmode?1:-1;e.hastranslate3d&&a.opt.enabletranslate3d?(this.setScrollTop=function(b,g){a.doc.translate.y=b;a.doc.translate.ty=-1*b+"px";a.doc.css(e.trstyle,"translate3d("+a.doc.translate.tx+","+a.doc.translate.ty+",0px)");g||a.notifyScrollEvent(a.win[0])},this.setScrollLeft=function(b,g){a.doc.translate.x=b;a.doc.translate.tx=b*K+"px";a.doc.css(e.trstyle,"translate3d("+
-a.doc.translate.tx+","+a.doc.translate.ty+",0px)");g||a.notifyScrollEvent(a.win[0])}):(this.setScrollTop=function(b,g){a.doc.translate.y=b;a.doc.translate.ty=-1*b+"px";a.doc.css(e.trstyle,"translate("+a.doc.translate.tx+","+a.doc.translate.ty+")");g||a.notifyScrollEvent(a.win[0])},this.setScrollLeft=function(b,g){a.doc.translate.x=b;a.doc.translate.tx=b*K+"px";a.doc.css(e.trstyle,"translate("+a.doc.translate.tx+","+a.doc.translate.ty+")");g||a.notifyScrollEvent(a.win[0])})}else this.getScrollTop=
-function(){return a.docscroll.scrollTop()},this.setScrollTop=function(b){return a.docscroll.scrollTop(b)},this.getScrollLeft=function(){return a.detected.ismozilla&&a.isrtlmode?Math.abs(a.docscroll.scrollLeft()):a.docscroll.scrollLeft()},this.setScrollLeft=function(b){return a.docscroll.scrollLeft(a.detected.ismozilla&&a.isrtlmode?-b:b)};this.getTarget=function(a){return a?a.target?a.target:a.srcElement?a.srcElement:!1:!1};this.hasParent=function(a,g){if(!a)return!1;for(var c=a.target||a.srcElement||
-a||!1;c&&c.id!=g;)c=c.parentNode||!1;return!1!==c};var w={thin:1,medium:3,thick:5};this.getDocumentScrollOffset=function(){return{top:window.pageYOffset||document.documentElement.scrollTop,left:window.pageXOffset||document.documentElement.scrollLeft}};this.getOffset=function(){if(a.isfixed){var b=a.win.offset(),g=a.getDocumentScrollOffset();b.top-=g.top;b.left-=g.left;return b}b=a.win.offset();if(!a.viewport)return b;g=a.viewport.offset();return{top:b.top-g.top,left:b.left-g.left}};this.updateScrollBar=
-function(b){if(a.ishwscroll)a.rail.css({height:a.win.innerHeight()-(a.opt.railpadding.top+a.opt.railpadding.bottom)}),a.railh&&a.railh.css({width:a.win.innerWidth()-(a.opt.railpadding.left+a.opt.railpadding.right)});else{var g=a.getOffset(),c=g.top,e=g.left-(a.opt.railpadding.left+a.opt.railpadding.right),c=c+d(a.win,"border-top-width",!0),e=e+(a.rail.align?a.win.outerWidth()-d(a.win,"border-right-width")-a.rail.width:d(a.win,"border-left-width")),f=a.opt.railoffset;f&&(f.top&&(c+=f.top),a.rail.align&&
-f.left&&(e+=f.left));a.railslocked||a.rail.css({top:c,left:e,height:(b?b.h:a.win.innerHeight())-(a.opt.railpadding.top+a.opt.railpadding.bottom)});a.zoom&&a.zoom.css({top:c+1,left:1==a.rail.align?e-20:e+a.rail.width+4});if(a.railh&&!a.railslocked){c=g.top;e=g.left;if(f=a.opt.railhoffset)f.top&&(c+=f.top),f.left&&(e+=f.left);b=a.railh.align?c+d(a.win,"border-top-width",!0)+a.win.innerHeight()-a.railh.height:c+d(a.win,"border-top-width",!0);e+=d(a.win,"border-left-width");a.railh.css({top:b-(a.opt.railpadding.top+
-a.opt.railpadding.bottom),left:e,width:a.railh.width})}}};this.doRailClick=function(b,g,c){var e;a.railslocked||(a.cancelEvent(b),g?(g=c?a.doScrollLeft:a.doScrollTop,e=c?(b.pageX-a.railh.offset().left-a.cursorwidth/2)*a.scrollratio.x:(b.pageY-a.rail.offset().top-a.cursorheight/2)*a.scrollratio.y,g(e)):(g=c?a.doScrollLeftBy:a.doScrollBy,e=c?a.scroll.x:a.scroll.y,b=c?b.pageX-a.railh.offset().left:b.pageY-a.rail.offset().top,c=c?a.view.w:a.view.h,g(e>=b?c:-c)))};a.hasanimationframe=s;a.hascancelanimationframe=
-t;a.hasanimationframe?a.hascancelanimationframe||(t=function(){a.cancelAnimationFrame=!0}):(s=function(a){return setTimeout(a,15-Math.floor(+new Date/1E3)%16)},t=clearInterval);this.init=function(){a.saved.css=[];if(e.isie7mobile||e.isoperamini)return!0;e.hasmstouch&&a.css(a.ispage?f("html"):a.win,{"-ms-touch-action":"none"});a.zindex="auto";a.zindex=a.ispage||"auto"!=a.opt.zindex?a.opt.zindex:m()||"auto";!a.ispage&&"auto"!=a.zindex&&a.zindex>x&&(x=a.zindex);a.isie&&0==a.zindex&&"auto"==a.opt.zindex&&
-(a.zindex="auto");if(!a.ispage||!e.cantouch&&!e.isieold&&!e.isie9mobile){var b=a.docscroll;a.ispage&&(b=a.haswrapper?a.win:a.doc);e.isie9mobile||a.css(b,{"overflow-y":"hidden"});a.ispage&&e.isie7&&("BODY"==a.doc[0].nodeName?a.css(f("html"),{"overflow-y":"hidden"}):"HTML"==a.doc[0].nodeName&&a.css(f("body"),{"overflow-y":"hidden"}));!e.isios||a.ispage||a.haswrapper||a.css(f("body"),{"-webkit-overflow-scrolling":"touch"});var g=f(document.createElement("div"));g.css({position:"relative",top:0,"float":"right",
-width:a.opt.cursorwidth,height:"0px","background-color":a.opt.cursorcolor,border:a.opt.cursorborder,"background-clip":"padding-box","-webkit-border-radius":a.opt.cursorborderradius,"-moz-border-radius":a.opt.cursorborderradius,"border-radius":a.opt.cursorborderradius});g.hborder=parseFloat(g.outerHeight()-g.innerHeight());g.addClass("nicescroll-cursors");a.cursor=g;var c=f(document.createElement("div"));c.attr("id",a.id);c.addClass("nicescroll-rails nicescroll-rails-vr");var d,h,k=["left","right",
-"top","bottom"],J;for(J in k)h=k[J],(d=a.opt.railpadding[h])?c.css("padding-"+h,d+"px"):a.opt.railpadding[h]=0;c.append(g);c.width=Math.max(parseFloat(a.opt.cursorwidth),g.outerWidth());c.css({width:c.width+"px",zIndex:a.zindex,background:a.opt.background,cursor:"default"});c.visibility=!0;c.scrollable=!0;c.align="left"==a.opt.railalign?0:1;a.rail=c;g=a.rail.drag=!1;!a.opt.boxzoom||a.ispage||e.isieold||(g=document.createElement("div"),a.bind(g,"click",a.doZoom),a.bind(g,"mouseenter",function(){a.zoom.css("opacity",
-a.opt.cursoropacitymax)}),a.bind(g,"mouseleave",function(){a.zoom.css("opacity",a.opt.cursoropacitymin)}),a.zoom=f(g),a.zoom.css({cursor:"pointer","z-index":a.zindex,backgroundImage:"url("+a.opt.scriptpath+"zoomico.png)",height:18,width:18,backgroundPosition:"0px 0px"}),a.opt.dblclickzoom&&a.bind(a.win,"dblclick",a.doZoom),e.cantouch&&a.opt.gesturezoom&&(a.ongesturezoom=function(b){1.5<b.scale&&a.doZoomIn(b);.8>b.scale&&a.doZoomOut(b);return a.cancelEvent(b)},a.bind(a.win,"gestureend",a.ongesturezoom)));
-a.railh=!1;var l;a.opt.horizrailenabled&&(a.css(b,{"overflow-x":"hidden"}),g=f(document.createElement("div")),g.css({position:"absolute",top:0,height:a.opt.cursorwidth,width:"0px","background-color":a.opt.cursorcolor,border:a.opt.cursorborder,"background-clip":"padding-box","-webkit-border-radius":a.opt.cursorborderradius,"-moz-border-radius":a.opt.cursorborderradius,"border-radius":a.opt.cursorborderradius}),e.isieold&&g.css({overflow:"hidden"}),g.wborder=parseFloat(g.outerWidth()-g.innerWidth()),
-g.addClass("nicescroll-cursors"),a.cursorh=g,l=f(document.createElement("div")),l.attr("id",a.id+"-hr"),l.addClass("nicescroll-rails nicescroll-rails-hr"),l.height=Math.max(parseFloat(a.opt.cursorwidth),g.outerHeight()),l.css({height:l.height+"px",zIndex:a.zindex,background:a.opt.background}),l.append(g),l.visibility=!0,l.scrollable=!0,l.align="top"==a.opt.railvalign?0:1,a.railh=l,a.railh.drag=!1);a.ispage?(c.css({position:"fixed",top:"0px",height:"100%"}),c.align?c.css({right:"0px"}):c.css({left:"0px"}),
-a.body.append(c),a.railh&&(l.css({position:"fixed",left:"0px",width:"100%"}),l.align?l.css({bottom:"0px"}):l.css({top:"0px"}),a.body.append(l))):(a.ishwscroll?("static"==a.win.css("position")&&a.css(a.win,{position:"relative"}),b="HTML"==a.win[0].nodeName?a.body:a.win,f(b).scrollTop(0).scrollLeft(0),a.zoom&&(a.zoom.css({position:"absolute",top:1,right:0,"margin-right":c.width+4}),b.append(a.zoom)),c.css({position:"absolute",top:0}),c.align?c.css({right:0}):c.css({left:0}),b.append(c),l&&(l.css({position:"absolute",
-left:0,bottom:0}),l.align?l.css({bottom:0}):l.css({top:0}),b.append(l))):(a.isfixed="fixed"==a.win.css("position"),b=a.isfixed?"fixed":"absolute",a.isfixed||(a.viewport=a.getViewport(a.win[0])),a.viewport&&(a.body=a.viewport,0==/fixed|absolute/.test(a.viewport.css("position"))&&a.css(a.viewport,{position:"relative"})),c.css({position:b}),a.zoom&&a.zoom.css({position:b}),a.updateScrollBar(),a.body.append(c),a.zoom&&a.body.append(a.zoom),a.railh&&(l.css({position:b}),a.body.append(l))),e.isios&&a.css(a.win,
-{"-webkit-tap-highlight-color":"rgba(0,0,0,0)","-webkit-touch-callout":"none"}),e.isie&&a.opt.disableoutline&&a.win.attr("hideFocus","true"),e.iswebkit&&a.opt.disableoutline&&a.win.css({outline:"none"}));!1===a.opt.autohidemode?(a.autohidedom=!1,a.rail.css({opacity:a.opt.cursoropacitymax}),a.railh&&a.railh.css({opacity:a.opt.cursoropacitymax})):!0===a.opt.autohidemode||"leave"===a.opt.autohidemode?(a.autohidedom=f().add(a.rail),e.isie8&&(a.autohidedom=a.autohidedom.add(a.cursor)),a.railh&&(a.autohidedom=
-a.autohidedom.add(a.railh)),a.railh&&e.isie8&&(a.autohidedom=a.autohidedom.add(a.cursorh))):"scroll"==a.opt.autohidemode?(a.autohidedom=f().add(a.rail),a.railh&&(a.autohidedom=a.autohidedom.add(a.railh))):"cursor"==a.opt.autohidemode?(a.autohidedom=f().add(a.cursor),a.railh&&(a.autohidedom=a.autohidedom.add(a.cursorh))):"hidden"==a.opt.autohidemode&&(a.autohidedom=!1,a.hide(),a.railslocked=!1);if(e.isie9mobile)a.scrollmom=new L(a),a.onmangotouch=function(){var b=a.getScrollTop(),c=a.getScrollLeft();
-if(b==a.scrollmom.lastscrolly&&c==a.scrollmom.lastscrollx)return!0;var g=b-a.mangotouch.sy,e=c-a.mangotouch.sx;if(0!=Math.round(Math.sqrt(Math.pow(e,2)+Math.pow(g,2)))){var d=0>g?-1:1,f=0>e?-1:1,q=+new Date;a.mangotouch.lazy&&clearTimeout(a.mangotouch.lazy);80<q-a.mangotouch.tm||a.mangotouch.dry!=d||a.mangotouch.drx!=f?(a.scrollmom.stop(),a.scrollmom.reset(c,b),a.mangotouch.sy=b,a.mangotouch.ly=b,a.mangotouch.sx=c,a.mangotouch.lx=c,a.mangotouch.dry=d,a.mangotouch.drx=f,a.mangotouch.tm=q):(a.scrollmom.stop(),
-a.scrollmom.update(a.mangotouch.sx-e,a.mangotouch.sy-g),a.mangotouch.tm=q,g=Math.max(Math.abs(a.mangotouch.ly-b),Math.abs(a.mangotouch.lx-c)),a.mangotouch.ly=b,a.mangotouch.lx=c,2<g&&(a.mangotouch.lazy=setTimeout(function(){a.mangotouch.lazy=!1;a.mangotouch.dry=0;a.mangotouch.drx=0;a.mangotouch.tm=0;a.scrollmom.doMomentum(30)},100)))}},c=a.getScrollTop(),l=a.getScrollLeft(),a.mangotouch={sy:c,ly:c,dry:0,sx:l,lx:l,drx:0,lazy:!1,tm:0},a.bind(a.docscroll,"scroll",a.onmangotouch);else{if(e.cantouch||
-a.istouchcapable||a.opt.touchbehavior||e.hasmstouch){a.scrollmom=new L(a);a.ontouchstart=function(b){if(b.pointerType&&2!=b.pointerType&&"touch"!=b.pointerType)return!1;a.hasmoving=!1;if(!a.railslocked){var c;if(e.hasmstouch)for(c=b.target?b.target:!1;c;){var g=f(c).getNiceScroll();if(0<g.length&&g[0].me==a.me)break;if(0<g.length)return!1;if("DIV"==c.nodeName&&c.id==a.id)break;c=c.parentNode?c.parentNode:!1}a.cancelScroll();if((c=a.getTarget(b))&&/INPUT/i.test(c.nodeName)&&/range/i.test(c.type))return a.stopPropagation(b);
-!("clientX"in b)&&"changedTouches"in b&&(b.clientX=b.changedTouches[0].clientX,b.clientY=b.changedTouches[0].clientY);a.forcescreen&&(g=b,b={original:b.original?b.original:b},b.clientX=g.screenX,b.clientY=g.screenY);a.rail.drag={x:b.clientX,y:b.clientY,sx:a.scroll.x,sy:a.scroll.y,st:a.getScrollTop(),sl:a.getScrollLeft(),pt:2,dl:!1};if(a.ispage||!a.opt.directionlockdeadzone)a.rail.drag.dl="f";else{var g=f(window).width(),d=f(window).height(),q=Math.max(document.body.scrollWidth,document.documentElement.scrollWidth),
-h=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight),d=Math.max(0,h-d),g=Math.max(0,q-g);a.rail.drag.ck=!a.rail.scrollable&&a.railh.scrollable?0<d?"v":!1:a.rail.scrollable&&!a.railh.scrollable?0<g?"h":!1:!1;a.rail.drag.ck||(a.rail.drag.dl="f")}a.opt.touchbehavior&&a.isiframe&&e.isie&&(g=a.win.position(),a.rail.drag.x+=g.left,a.rail.drag.y+=g.top);a.hasmoving=!1;a.lastmouseup=!1;a.scrollmom.reset(b.clientX,b.clientY);if(!e.cantouch&&!this.istouchcapable&&!b.pointerType){if(!c||
-!/INPUT|SELECT|TEXTAREA/i.test(c.nodeName))return!a.ispage&&e.hasmousecapture&&c.setCapture(),a.opt.touchbehavior?(c.onclick&&!c._onclick&&(c._onclick=c.onclick,c.onclick=function(b){if(a.hasmoving)return!1;c._onclick.call(this,b)}),a.cancelEvent(b)):a.stopPropagation(b);/SUBMIT|CANCEL|BUTTON/i.test(f(c).attr("type"))&&(pc={tg:c,click:!1},a.preventclick=pc)}}};a.ontouchend=function(b){if(!a.rail.drag)return!0;if(2==a.rail.drag.pt){if(b.pointerType&&2!=b.pointerType&&"touch"!=b.pointerType)return!1;
-a.scrollmom.doMomentum();a.rail.drag=!1;if(a.hasmoving&&(a.lastmouseup=!0,a.hideCursor(),e.hasmousecapture&&document.releaseCapture(),!e.cantouch))return a.cancelEvent(b)}else if(1==a.rail.drag.pt)return a.onmouseup(b)};var n=a.opt.touchbehavior&&a.isiframe&&!e.hasmousecapture;a.ontouchmove=function(b,c){if(!a.rail.drag||b.targetTouches&&a.opt.preventmultitouchscrolling&&1<b.targetTouches.length||b.pointerType&&2!=b.pointerType&&"touch"!=b.pointerType)return!1;if(2==a.rail.drag.pt){if(e.cantouch&&
-e.isios&&"undefined"==typeof b.original)return!0;a.hasmoving=!0;a.preventclick&&!a.preventclick.click&&(a.preventclick.click=a.preventclick.tg.onclick||!1,a.preventclick.tg.onclick=a.onpreventclick);b=f.extend({original:b},b);"changedTouches"in b&&(b.clientX=b.changedTouches[0].clientX,b.clientY=b.changedTouches[0].clientY);if(a.forcescreen){var g=b;b={original:b.original?b.original:b};b.clientX=g.screenX;b.clientY=g.screenY}var d,g=d=0;n&&!c&&(d=a.win.position(),g=-d.left,d=-d.top);var q=b.clientY+
-d;d=q-a.rail.drag.y;var h=b.clientX+g,u=h-a.rail.drag.x,k=a.rail.drag.st-d;a.ishwscroll&&a.opt.bouncescroll?0>k?k=Math.round(k/2):k>a.page.maxh&&(k=a.page.maxh+Math.round((k-a.page.maxh)/2)):(0>k&&(q=k=0),k>a.page.maxh&&(k=a.page.maxh,q=0));var l;a.railh&&a.railh.scrollable&&(l=a.isrtlmode?u-a.rail.drag.sl:a.rail.drag.sl-u,a.ishwscroll&&a.opt.bouncescroll?0>l?l=Math.round(l/2):l>a.page.maxw&&(l=a.page.maxw+Math.round((l-a.page.maxw)/2)):(0>l&&(h=l=0),l>a.page.maxw&&(l=a.page.maxw,h=0)));g=!1;if(a.rail.drag.dl)g=
-!0,"v"==a.rail.drag.dl?l=a.rail.drag.sl:"h"==a.rail.drag.dl&&(k=a.rail.drag.st);else{d=Math.abs(d);var u=Math.abs(u),z=a.opt.directionlockdeadzone;if("v"==a.rail.drag.ck){if(d>z&&u<=.3*d)return a.rail.drag=!1,!0;u>z&&(a.rail.drag.dl="f",f("body").scrollTop(f("body").scrollTop()))}else if("h"==a.rail.drag.ck){if(u>z&&d<=.3*u)return a.rail.drag=!1,!0;d>z&&(a.rail.drag.dl="f",f("body").scrollLeft(f("body").scrollLeft()))}}a.synched("touchmove",function(){a.rail.drag&&2==a.rail.drag.pt&&(a.prepareTransition&&
-a.prepareTransition(0),a.rail.scrollable&&a.setScrollTop(k),a.scrollmom.update(h,q),a.railh&&a.railh.scrollable?(a.setScrollLeft(l),a.showCursor(k,l)):a.showCursor(k),e.isie10&&document.selection.clear())});e.ischrome&&a.istouchcapable&&(g=!1);if(g)return a.cancelEvent(b)}else if(1==a.rail.drag.pt)return a.onmousemove(b)}}a.onmousedown=function(b,c){if(!a.rail.drag||1==a.rail.drag.pt){if(a.railslocked)return a.cancelEvent(b);a.cancelScroll();a.rail.drag={x:b.clientX,y:b.clientY,sx:a.scroll.x,sy:a.scroll.y,
-pt:1,hr:!!c};var g=a.getTarget(b);!a.ispage&&e.hasmousecapture&&g.setCapture();a.isiframe&&!e.hasmousecapture&&(a.saved.csspointerevents=a.doc.css("pointer-events"),a.css(a.doc,{"pointer-events":"none"}));a.hasmoving=!1;return a.cancelEvent(b)}};a.onmouseup=function(b){if(a.rail.drag){if(1!=a.rail.drag.pt)return!0;e.hasmousecapture&&document.releaseCapture();a.isiframe&&!e.hasmousecapture&&a.doc.css("pointer-events",a.saved.csspointerevents);a.rail.drag=!1;a.hasmoving&&a.triggerScrollEnd();return a.cancelEvent(b)}};
-a.onmousemove=function(b){if(a.rail.drag&&1==a.rail.drag.pt){if(e.ischrome&&0==b.which)return a.onmouseup(b);a.cursorfreezed=!0;a.hasmoving=!0;if(a.rail.drag.hr){a.scroll.x=a.rail.drag.sx+(b.clientX-a.rail.drag.x);0>a.scroll.x&&(a.scroll.x=0);var c=a.scrollvaluemaxw;a.scroll.x>c&&(a.scroll.x=c)}else a.scroll.y=a.rail.drag.sy+(b.clientY-a.rail.drag.y),0>a.scroll.y&&(a.scroll.y=0),c=a.scrollvaluemax,a.scroll.y>c&&(a.scroll.y=c);a.synched("mousemove",function(){a.rail.drag&&1==a.rail.drag.pt&&(a.showCursor(),
-a.rail.drag.hr?a.hasreversehr?a.doScrollLeft(a.scrollvaluemaxw-Math.round(a.scroll.x*a.scrollratio.x),a.opt.cursordragspeed):a.doScrollLeft(Math.round(a.scroll.x*a.scrollratio.x),a.opt.cursordragspeed):a.doScrollTop(Math.round(a.scroll.y*a.scrollratio.y),a.opt.cursordragspeed))});return a.cancelEvent(b)}};if(e.cantouch||a.opt.touchbehavior)a.onpreventclick=function(b){if(a.preventclick)return a.preventclick.tg.onclick=a.preventclick.click,a.preventclick=!1,a.cancelEvent(b)},a.bind(a.win,"mousedown",
-a.ontouchstart),a.onclick=e.isios?!1:function(b){return a.lastmouseup?(a.lastmouseup=!1,a.cancelEvent(b)):!0},a.opt.grabcursorenabled&&e.cursorgrabvalue&&(a.css(a.ispage?a.doc:a.win,{cursor:e.cursorgrabvalue}),a.css(a.rail,{cursor:e.cursorgrabvalue}));else{var p=function(b){if(a.selectiondrag){if(b){var c=a.win.outerHeight();b=b.pageY-a.selectiondrag.top;0<b&&b<c&&(b=0);b>=c&&(b-=c);a.selectiondrag.df=b}0!=a.selectiondrag.df&&(a.doScrollBy(2*-Math.floor(a.selectiondrag.df/6)),a.debounced("doselectionscroll",
-function(){p()},50))}};a.hasTextSelected="getSelection"in document?function(){return 0<document.getSelection().rangeCount}:"selection"in document?function(){return"None"!=document.selection.type}:function(){return!1};a.onselectionstart=function(b){a.ispage||(a.selectiondrag=a.win.offset())};a.onselectionend=function(b){a.selectiondrag=!1};a.onselectiondrag=function(b){a.selectiondrag&&a.hasTextSelected()&&a.debounced("selectionscroll",function(){p(b)},250)}}e.hasw3ctouch?(a.css(a.rail,{"touch-action":"none"}),
-a.css(a.cursor,{"touch-action":"none"}),a.bind(a.win,"pointerdown",a.ontouchstart),a.bind(document,"pointerup",a.ontouchend),a.bind(document,"pointermove",a.ontouchmove)):e.hasmstouch?(a.css(a.rail,{"-ms-touch-action":"none"}),a.css(a.cursor,{"-ms-touch-action":"none"}),a.bind(a.win,"MSPointerDown",a.ontouchstart),a.bind(document,"MSPointerUp",a.ontouchend),a.bind(document,"MSPointerMove",a.ontouchmove),a.bind(a.cursor,"MSGestureHold",function(a){a.preventDefault()}),a.bind(a.cursor,"contextmenu",
-function(a){a.preventDefault()})):this.istouchcapable&&(a.bind(a.win,"touchstart",a.ontouchstart),a.bind(document,"touchend",a.ontouchend),a.bind(document,"touchcancel",a.ontouchend),a.bind(document,"touchmove",a.ontouchmove));if(a.opt.cursordragontouch||!e.cantouch&&!a.opt.touchbehavior)a.rail.css({cursor:"default"}),a.railh&&a.railh.css({cursor:"default"}),a.jqbind(a.rail,"mouseenter",function(){if(!a.ispage&&!a.win.is(":visible"))return!1;a.canshowonmouseevent&&a.showCursor();a.rail.active=!0}),
-a.jqbind(a.rail,"mouseleave",function(){a.rail.active=!1;a.rail.drag||a.hideCursor()}),a.opt.sensitiverail&&(a.bind(a.rail,"click",function(b){a.doRailClick(b,!1,!1)}),a.bind(a.rail,"dblclick",function(b){a.doRailClick(b,!0,!1)}),a.bind(a.cursor,"click",function(b){a.cancelEvent(b)}),a.bind(a.cursor,"dblclick",function(b){a.cancelEvent(b)})),a.railh&&(a.jqbind(a.railh,"mouseenter",function(){if(!a.ispage&&!a.win.is(":visible"))return!1;a.canshowonmouseevent&&a.showCursor();a.rail.active=!0}),a.jqbind(a.railh,
-"mouseleave",function(){a.rail.active=!1;a.rail.drag||a.hideCursor()}),a.opt.sensitiverail&&(a.bind(a.railh,"click",function(b){a.doRailClick(b,!1,!0)}),a.bind(a.railh,"dblclick",function(b){a.doRailClick(b,!0,!0)}),a.bind(a.cursorh,"click",function(b){a.cancelEvent(b)}),a.bind(a.cursorh,"dblclick",function(b){a.cancelEvent(b)})));e.cantouch||a.opt.touchbehavior?(a.bind(e.hasmousecapture?a.win:document,"mouseup",a.ontouchend),a.bind(document,"mousemove",a.ontouchmove),a.onclick&&a.bind(document,"click",
-a.onclick),a.opt.cursordragontouch&&(a.bind(a.cursor,"mousedown",a.onmousedown),a.bind(a.cursor,"mouseup",a.onmouseup),a.cursorh&&a.bind(a.cursorh,"mousedown",function(b){a.onmousedown(b,!0)}),a.cursorh&&a.bind(a.cursorh,"mouseup",a.onmouseup))):(a.bind(e.hasmousecapture?a.win:document,"mouseup",a.onmouseup),a.bind(document,"mousemove",a.onmousemove),a.onclick&&a.bind(document,"click",a.onclick),a.bind(a.cursor,"mousedown",a.onmousedown),a.bind(a.cursor,"mouseup",a.onmouseup),a.railh&&(a.bind(a.cursorh,
-"mousedown",function(b){a.onmousedown(b,!0)}),a.bind(a.cursorh,"mouseup",a.onmouseup)),!a.ispage&&a.opt.enablescrollonselection&&(a.bind(a.win[0],"mousedown",a.onselectionstart),a.bind(document,"mouseup",a.onselectionend),a.bind(a.cursor,"mouseup",a.onselectionend),a.cursorh&&a.bind(a.cursorh,"mouseup",a.onselectionend),a.bind(document,"mousemove",a.onselectiondrag)),a.zoom&&(a.jqbind(a.zoom,"mouseenter",function(){a.canshowonmouseevent&&a.showCursor();a.rail.active=!0}),a.jqbind(a.zoom,"mouseleave",
-function(){a.rail.active=!1;a.rail.drag||a.hideCursor()})));a.opt.enablemousewheel&&(a.isiframe||a.bind(e.isie&&a.ispage?document:a.win,"mousewheel",a.onmousewheel),a.bind(a.rail,"mousewheel",a.onmousewheel),a.railh&&a.bind(a.railh,"mousewheel",a.onmousewheelhr));a.ispage||e.cantouch||/HTML|^BODY/.test(a.win[0].nodeName)||(a.win.attr("tabindex")||a.win.attr({tabindex:N++}),a.jqbind(a.win,"focus",function(b){y=a.getTarget(b).id||!0;a.hasfocus=!0;a.canshowonmouseevent&&a.noticeCursor()}),a.jqbind(a.win,
-"blur",function(b){y=!1;a.hasfocus=!1}),a.jqbind(a.win,"mouseenter",function(b){D=a.getTarget(b).id||!0;a.hasmousefocus=!0;a.canshowonmouseevent&&a.noticeCursor()}),a.jqbind(a.win,"mouseleave",function(){D=!1;a.hasmousefocus=!1;a.rail.drag||a.hideCursor()}))}a.onkeypress=function(b){if(a.railslocked&&0==a.page.maxh)return!0;b=b?b:window.e;var c=a.getTarget(b);if(c&&/INPUT|TEXTAREA|SELECT|OPTION/.test(c.nodeName)&&(!c.getAttribute("type")&&!c.type||!/submit|button|cancel/i.tp)||f(c).attr("contenteditable"))return!0;
-if(a.hasfocus||a.hasmousefocus&&!y||a.ispage&&!y&&!D){c=b.keyCode;if(a.railslocked&&27!=c)return a.cancelEvent(b);var g=b.ctrlKey||!1,d=b.shiftKey||!1,e=!1;switch(c){case 38:case 63233:a.doScrollBy(72);e=!0;break;case 40:case 63235:a.doScrollBy(-72);e=!0;break;case 37:case 63232:a.railh&&(g?a.doScrollLeft(0):a.doScrollLeftBy(72),e=!0);break;case 39:case 63234:a.railh&&(g?a.doScrollLeft(a.page.maxw):a.doScrollLeftBy(-72),e=!0);break;case 33:case 63276:a.doScrollBy(a.view.h);e=!0;break;case 34:case 63277:a.doScrollBy(-a.view.h);
-e=!0;break;case 36:case 63273:a.railh&&g?a.doScrollPos(0,0):a.doScrollTo(0);e=!0;break;case 35:case 63275:a.railh&&g?a.doScrollPos(a.page.maxw,a.page.maxh):a.doScrollTo(a.page.maxh);e=!0;break;case 32:a.opt.spacebarenabled&&(d?a.doScrollBy(a.view.h):a.doScrollBy(-a.view.h),e=!0);break;case 27:a.zoomactive&&(a.doZoom(),e=!0)}if(e)return a.cancelEvent(b)}};a.opt.enablekeyboard&&a.bind(document,e.isopera&&!e.isopera12?"keypress":"keydown",a.onkeypress);a.bind(document,"keydown",function(b){b.ctrlKey&&
-(a.wheelprevented=!0)});a.bind(document,"keyup",function(b){b.ctrlKey||(a.wheelprevented=!1)});a.bind(window,"blur",function(b){a.wheelprevented=!1});a.bind(window,"resize",a.lazyResize);a.bind(window,"orientationchange",a.lazyResize);a.bind(window,"load",a.lazyResize);if(e.ischrome&&!a.ispage&&!a.haswrapper){var r=a.win.attr("style"),c=parseFloat(a.win.css("width"))+1;a.win.css("width",c);a.synched("chromefix",function(){a.win.attr("style",r)})}a.onAttributeChange=function(b){a.lazyResize(a.isieold?
-250:30)};!1!==v&&(a.observerbody=new v(function(b){b.forEach(function(b){if("attributes"==b.type)return f("body").hasClass("modal-open")?a.hide():a.show()});if(document.body.scrollHeight!=a.page.maxh)return a.lazyResize(30)}),a.observerbody.observe(document.body,{childList:!0,subtree:!0,characterData:!1,attributes:!0,attributeFilter:["class"]}));a.ispage||a.haswrapper||(!1!==v?(a.observer=new v(function(b){b.forEach(a.onAttributeChange)}),a.observer.observe(a.win[0],{childList:!0,characterData:!1,
-attributes:!0,subtree:!1}),a.observerremover=new v(function(b){b.forEach(function(b){if(0<b.removedNodes.length)for(var c in b.removedNodes)if(a&&b.removedNodes[c]==a.win[0])return a.remove()})}),a.observerremover.observe(a.win[0].parentNode,{childList:!0,characterData:!1,attributes:!1,subtree:!1})):(a.bind(a.win,e.isie&&!e.isie9?"propertychange":"DOMAttrModified",a.onAttributeChange),e.isie9&&a.win[0].attachEvent("onpropertychange",a.onAttributeChange),a.bind(a.win,"DOMNodeRemoved",function(b){b.target==
-a.win[0]&&a.remove()})));!a.ispage&&a.opt.boxzoom&&a.bind(window,"resize",a.resizeZoom);a.istextarea&&a.bind(a.win,"mouseup",a.lazyResize);a.lazyResize(30)}if("IFRAME"==this.doc[0].nodeName){var M=function(){a.iframexd=!1;var b;try{b="contentDocument"in this?this.contentDocument:this.contentWindow.document}catch(c){a.iframexd=!0,b=!1}if(a.iframexd)return"console"in window&&console.log("NiceScroll error: policy restriced iframe"),!0;a.forcescreen=!0;a.isiframe&&(a.iframe={doc:f(b),html:a.doc.contents().find("html")[0],
-body:a.doc.contents().find("body")[0]},a.getContentSize=function(){return{w:Math.max(a.iframe.html.scrollWidth,a.iframe.body.scrollWidth),h:Math.max(a.iframe.html.scrollHeight,a.iframe.body.scrollHeight)}},a.docscroll=f(a.iframe.body));if(!e.isios&&a.opt.iframeautoresize&&!a.isiframe){a.win.scrollTop(0);a.doc.height("");var g=Math.max(b.getElementsByTagName("html")[0].scrollHeight,b.body.scrollHeight);a.doc.height(g)}a.lazyResize(30);e.isie7&&a.css(f(a.iframe.html),{"overflow-y":"hidden"});a.css(f(a.iframe.body),
-{"overflow-y":"hidden"});e.isios&&a.haswrapper&&a.css(f(b.body),{"-webkit-transform":"translate3d(0,0,0)"});"contentWindow"in this?a.bind(this.contentWindow,"scroll",a.onscroll):a.bind(b,"scroll",a.onscroll);a.opt.enablemousewheel&&a.bind(b,"mousewheel",a.onmousewheel);a.opt.enablekeyboard&&a.bind(b,e.isopera?"keypress":"keydown",a.onkeypress);if(e.cantouch||a.opt.touchbehavior)a.bind(b,"mousedown",a.ontouchstart),a.bind(b,"mousemove",function(b){return a.ontouchmove(b,!0)}),a.opt.grabcursorenabled&&
-e.cursorgrabvalue&&a.css(f(b.body),{cursor:e.cursorgrabvalue});a.bind(b,"mouseup",a.ontouchend);a.zoom&&(a.opt.dblclickzoom&&a.bind(b,"dblclick",a.doZoom),a.ongesturezoom&&a.bind(b,"gestureend",a.ongesturezoom))};this.doc[0].readyState&&"complete"==this.doc[0].readyState&&setTimeout(function(){M.call(a.doc[0],!1)},500);a.bind(this.doc,"load",M)}};this.showCursor=function(b,c){a.cursortimeout&&(clearTimeout(a.cursortimeout),a.cursortimeout=0);if(a.rail){a.autohidedom&&(a.autohidedom.stop().css({opacity:a.opt.cursoropacitymax}),
-a.cursoractive=!0);a.rail.drag&&1==a.rail.drag.pt||("undefined"!=typeof b&&!1!==b&&(a.scroll.y=Math.round(1*b/a.scrollratio.y)),"undefined"!=typeof c&&(a.scroll.x=Math.round(1*c/a.scrollratio.x)));a.cursor.css({height:a.cursorheight,top:a.scroll.y});if(a.cursorh){var d=a.hasreversehr?a.scrollvaluemaxw-a.scroll.x:a.scroll.x;!a.rail.align&&a.rail.visibility?a.cursorh.css({width:a.cursorwidth,left:d+a.rail.width}):a.cursorh.css({width:a.cursorwidth,left:d});a.cursoractive=!0}a.zoom&&a.zoom.stop().css({opacity:a.opt.cursoropacitymax})}};
-this.hideCursor=function(b){a.cursortimeout||!a.rail||!a.autohidedom||a.hasmousefocus&&"leave"==a.opt.autohidemode||(a.cursortimeout=setTimeout(function(){a.rail.active&&a.showonmouseevent||(a.autohidedom.stop().animate({opacity:a.opt.cursoropacitymin}),a.zoom&&a.zoom.stop().animate({opacity:a.opt.cursoropacitymin}),a.cursoractive=!1);a.cursortimeout=0},b||a.opt.hidecursordelay))};this.noticeCursor=function(b,c,d){a.showCursor(c,d);a.rail.active||a.hideCursor(b)};this.getContentSize=a.ispage?function(){return{w:Math.max(document.body.scrollWidth,
-document.documentElement.scrollWidth),h:Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}}:a.haswrapper?function(){return{w:a.doc.outerWidth()+parseInt(a.win.css("paddingLeft"))+parseInt(a.win.css("paddingRight")),h:a.doc.outerHeight()+parseInt(a.win.css("paddingTop"))+parseInt(a.win.css("paddingBottom"))}}:function(){return{w:a.docscroll[0].scrollWidth,h:a.docscroll[0].scrollHeight}};this.onResize=function(b,c){if(!a||!a.win)return!1;if(!a.haswrapper&&!a.ispage){if("none"==
-a.win.css("display"))return a.visibility&&a.hideRail().hideRailHr(),!1;a.hidden||a.visibility||a.showRail().showRailHr()}var d=a.page.maxh,e=a.page.maxw,f=a.view.h,h=a.view.w;a.view={w:a.ispage?a.win.width():parseInt(a.win[0].clientWidth),h:a.ispage?a.win.height():parseInt(a.win[0].clientHeight)};a.page=c?c:a.getContentSize();a.page.maxh=Math.max(0,a.page.h-a.view.h);a.page.maxw=Math.max(0,a.page.w-a.view.w);if(a.page.maxh==d&&a.page.maxw==e&&a.view.w==h&&a.view.h==f){if(a.ispage)return a;d=a.win.offset();
-if(a.lastposition&&(e=a.lastposition,e.top==d.top&&e.left==d.left))return a;a.lastposition=d}0==a.page.maxh?(a.hideRail(),a.scrollvaluemax=0,a.scroll.y=0,a.scrollratio.y=0,a.cursorheight=0,a.setScrollTop(0),a.rail.scrollable=!1):(a.page.maxh-=a.opt.railpadding.top+a.opt.railpadding.bottom,a.rail.scrollable=!0);0==a.page.maxw?(a.hideRailHr(),a.scrollvaluemaxw=0,a.scroll.x=0,a.scrollratio.x=0,a.cursorwidth=0,a.setScrollLeft(0),a.railh.scrollable=!1):(a.page.maxw-=a.opt.railpadding.left+a.opt.railpadding.right,
-a.railh.scrollable=!0);a.railslocked=a.locked||0==a.page.maxh&&0==a.page.maxw;if(a.railslocked)return a.ispage||a.updateScrollBar(a.view),!1;a.hidden||a.visibility?a.hidden||a.railh.visibility||a.showRailHr():a.showRail().showRailHr();a.istextarea&&a.win.css("resize")&&"none"!=a.win.css("resize")&&(a.view.h-=20);a.cursorheight=Math.min(a.view.h,Math.round(a.view.h/a.page.h*a.view.h));a.cursorheight=a.opt.cursorfixedheight?a.opt.cursorfixedheight:Math.max(a.opt.cursorminheight,a.cursorheight);a.cursorwidth=
-Math.min(a.view.w,Math.round(a.view.w/a.page.w*a.view.w));a.cursorwidth=a.opt.cursorfixedheight?a.opt.cursorfixedheight:Math.max(a.opt.cursorminheight,a.cursorwidth);a.scrollvaluemax=a.view.h-a.cursorheight-a.cursor.hborder-(a.opt.railpadding.top+a.opt.railpadding.bottom);a.railh&&(a.railh.width=0<a.page.maxh?a.view.w-a.rail.width:a.view.w,a.scrollvaluemaxw=a.railh.width-a.cursorwidth-a.cursorh.wborder-(a.opt.railpadding.left+a.opt.railpadding.right));a.ispage||a.updateScrollBar(a.view);a.scrollratio=
-{x:a.page.maxw/a.scrollvaluemaxw,y:a.page.maxh/a.scrollvaluemax};a.getScrollTop()>a.page.maxh?a.doScrollTop(a.page.maxh):(a.scroll.y=Math.round(a.getScrollTop()*(1/a.scrollratio.y)),a.scroll.x=Math.round(a.getScrollLeft()*(1/a.scrollratio.x)),a.cursoractive&&a.noticeCursor());a.scroll.y&&0==a.getScrollTop()&&a.doScrollTo(Math.floor(a.scroll.y*a.scrollratio.y));return a};this.resize=a.onResize;this.lazyResize=function(b){b=isNaN(b)?30:b;a.debounced("resize",a.resize,b);return a};this.jqbind=function(b,
-c,d){a.events.push({e:b,n:c,f:d,q:!0});f(b).bind(c,d)};this.bind=function(b,c,d,f){var h="jquery"in b?b[0]:b;"mousewheel"==c?window.addEventListener||"onwheel"in document?a._bind(h,"wheel",d,f||!1):(b="undefined"!=typeof document.onmousewheel?"mousewheel":"DOMMouseScroll",n(h,b,d,f||!1),"DOMMouseScroll"==b&&n(h,"MozMousePixelScroll",d,f||!1)):h.addEventListener?(e.cantouch&&/mouseup|mousedown|mousemove/.test(c)&&a._bind(h,"mousedown"==c?"touchstart":"mouseup"==c?"touchend":"touchmove",function(a){if(a.touches){if(2>
-a.touches.length){var b=a.touches.length?a.touches[0]:a;b.original=a;d.call(this,b)}}else a.changedTouches&&(b=a.changedTouches[0],b.original=a,d.call(this,b))},f||!1),a._bind(h,c,d,f||!1),e.cantouch&&"mouseup"==c&&a._bind(h,"touchcancel",d,f||!1)):a._bind(h,c,function(b){(b=b||window.event||!1)&&b.srcElement&&(b.target=b.srcElement);"pageY"in b||(b.pageX=b.clientX+document.documentElement.scrollLeft,b.pageY=b.clientY+document.documentElement.scrollTop);return!1===d.call(h,b)||!1===f?a.cancelEvent(b):
-!0})};e.haseventlistener?(this._bind=function(b,c,d,e){a.events.push({e:b,n:c,f:d,b:e,q:!1});b.addEventListener(c,d,e||!1)},this.cancelEvent=function(a){if(!a)return!1;a=a.original?a.original:a;a.preventDefault();a.stopPropagation();a.preventManipulation&&a.preventManipulation();return!1},this.stopPropagation=function(a){if(!a)return!1;a=a.original?a.original:a;a.stopPropagation();return!1},this._unbind=function(a,c,d,e){a.removeEventListener(c,d,e)}):(this._bind=function(b,c,d,e){a.events.push({e:b,
-n:c,f:d,b:e,q:!1});b.attachEvent?b.attachEvent("on"+c,d):b["on"+c]=d},this.cancelEvent=function(a){a=window.event||!1;if(!a)return!1;a.cancelBubble=!0;a.cancel=!0;return a.returnValue=!1},this.stopPropagation=function(a){a=window.event||!1;if(!a)return!1;a.cancelBubble=!0;return!1},this._unbind=function(a,c,d,e){a.detachEvent?a.detachEvent("on"+c,d):a["on"+c]=!1});this.unbindAll=function(){for(var b=0;b<a.events.length;b++){var c=a.events[b];c.q?c.e.unbind(c.n,c.f):a._unbind(c.e,c.n,c.f,c.b)}};this.showRail=
-function(){0==a.page.maxh||!a.ispage&&"none"==a.win.css("display")||(a.visibility=!0,a.rail.visibility=!0,a.rail.css("display","block"));return a};this.showRailHr=function(){if(!a.railh)return a;0==a.page.maxw||!a.ispage&&"none"==a.win.css("display")||(a.railh.visibility=!0,a.railh.css("display","block"));return a};this.hideRail=function(){a.visibility=!1;a.rail.visibility=!1;a.rail.css("display","none");return a};this.hideRailHr=function(){if(!a.railh)return a;a.railh.visibility=!1;a.railh.css("display",
-"none");return a};this.show=function(){a.hidden=!1;a.railslocked=!1;return a.showRail().showRailHr()};this.hide=function(){a.hidden=!0;a.railslocked=!0;return a.hideRail().hideRailHr()};this.toggle=function(){return a.hidden?a.show():a.hide()};this.remove=function(){a.stop();a.cursortimeout&&clearTimeout(a.cursortimeout);a.doZoomOut();a.unbindAll();e.isie9&&a.win[0].detachEvent("onpropertychange",a.onAttributeChange);!1!==a.observer&&a.observer.disconnect();!1!==a.observerremover&&a.observerremover.disconnect();
-!1!==a.observerbody&&a.observerbody.disconnect();a.events=null;a.cursor&&a.cursor.remove();a.cursorh&&a.cursorh.remove();a.rail&&a.rail.remove();a.railh&&a.railh.remove();a.zoom&&a.zoom.remove();for(var b=0;b<a.saved.css.length;b++){var c=a.saved.css[b];c[0].css(c[1],"undefined"==typeof c[2]?"":c[2])}a.saved=!1;a.me.data("__nicescroll","");var d=f.nicescroll;d.each(function(b){if(this&&this.id===a.id){delete d[b];for(var c=++b;c<d.length;c++,b++)d[b]=d[c];d.length--;d.length&&delete d[d.length]}});
-for(var h in a)a[h]=null,delete a[h];a=null};this.scrollstart=function(b){this.onscrollstart=b;return a};this.scrollend=function(b){this.onscrollend=b;return a};this.scrollcancel=function(b){this.onscrollcancel=b;return a};this.zoomin=function(b){this.onzoomin=b;return a};this.zoomout=function(b){this.onzoomout=b;return a};this.isScrollable=function(a){a=a.target?a.target:a;if("OPTION"==a.nodeName)return!0;for(;a&&1==a.nodeType&&!/^BODY|HTML/.test(a.nodeName);){var c=f(a),c=c.css("overflowY")||c.css("overflowX")||
-c.css("overflow")||"";if(/scroll|auto/.test(c))return a.clientHeight!=a.scrollHeight;a=a.parentNode?a.parentNode:!1}return!1};this.getViewport=function(a){for(a=a&&a.parentNode?a.parentNode:!1;a&&1==a.nodeType&&!/^BODY|HTML/.test(a.nodeName);){var c=f(a);if(/fixed|absolute/.test(c.css("position")))return c;var d=c.css("overflowY")||c.css("overflowX")||c.css("overflow")||"";if(/scroll|auto/.test(d)&&a.clientHeight!=a.scrollHeight||0<c.getNiceScroll().length)return c;a=a.parentNode?a.parentNode:!1}return!1};
-this.triggerScrollEnd=function(){if(a.onscrollend){var b=a.getScrollLeft(),c=a.getScrollTop();a.onscrollend.call(a,{type:"scrollend",current:{x:b,y:c},end:{x:b,y:c}})}};this.onmousewheel=function(b){if(!a.wheelprevented){if(a.railslocked)return a.debounced("checkunlock",a.resize,250),!0;if(a.rail.drag)return a.cancelEvent(b);"auto"==a.opt.oneaxismousemode&&0!=b.deltaX&&(a.opt.oneaxismousemode=!1);if(a.opt.oneaxismousemode&&0==b.deltaX&&!a.rail.scrollable)return a.railh&&a.railh.scrollable?a.onmousewheelhr(b):
-!0;var c=+new Date,d=!1;a.opt.preservenativescrolling&&a.checkarea+600<c&&(a.nativescrollingarea=a.isScrollable(b),d=!0);a.checkarea=c;if(a.nativescrollingarea)return!0;if(b=p(b,!1,d))a.checkarea=0;return b}};this.onmousewheelhr=function(b){if(!a.wheelprevented){if(a.railslocked||!a.railh.scrollable)return!0;if(a.rail.drag)return a.cancelEvent(b);var c=+new Date,d=!1;a.opt.preservenativescrolling&&a.checkarea+600<c&&(a.nativescrollingarea=a.isScrollable(b),d=!0);a.checkarea=c;return a.nativescrollingarea?
-!0:a.railslocked?a.cancelEvent(b):p(b,!0,d)}};this.stop=function(){a.cancelScroll();a.scrollmon&&a.scrollmon.stop();a.cursorfreezed=!1;a.scroll.y=Math.round(a.getScrollTop()*(1/a.scrollratio.y));a.noticeCursor();return a};this.getTransitionSpeed=function(b){var c=Math.round(10*a.opt.scrollspeed);b=Math.min(c,Math.round(b/20*a.opt.scrollspeed));return 20<b?b:0};a.opt.smoothscroll?a.ishwscroll&&e.hastransition&&a.opt.usetransition&&a.opt.smoothscroll?(this.prepareTransition=function(b,c){var d=c?20<
-b?b:0:a.getTransitionSpeed(b),f=d?e.prefixstyle+"transform "+d+"ms ease-out":"";a.lasttransitionstyle&&a.lasttransitionstyle==f||(a.lasttransitionstyle=f,a.doc.css(e.transitionstyle,f));return d},this.doScrollLeft=function(b,c){var d=a.scrollrunning?a.newscrolly:a.getScrollTop();a.doScrollPos(b,d,c)},this.doScrollTop=function(b,c){var d=a.scrollrunning?a.newscrollx:a.getScrollLeft();a.doScrollPos(d,b,c)},this.doScrollPos=function(b,c,d){var f=a.getScrollTop(),h=a.getScrollLeft();(0>(a.newscrolly-
-f)*(c-f)||0>(a.newscrollx-h)*(b-h))&&a.cancelScroll();0==a.opt.bouncescroll&&(0>c?c=0:c>a.page.maxh&&(c=a.page.maxh),0>b?b=0:b>a.page.maxw&&(b=a.page.maxw));if(a.scrollrunning&&b==a.newscrollx&&c==a.newscrolly)return!1;a.newscrolly=c;a.newscrollx=b;a.newscrollspeed=d||!1;if(a.timer)return!1;a.timer=setTimeout(function(){var d=a.getScrollTop(),f=a.getScrollLeft(),h,k;h=b-f;k=c-d;h=Math.round(Math.sqrt(Math.pow(h,2)+Math.pow(k,2)));h=a.newscrollspeed&&1<a.newscrollspeed?a.newscrollspeed:a.getTransitionSpeed(h);
-a.newscrollspeed&&1>=a.newscrollspeed&&(h*=a.newscrollspeed);a.prepareTransition(h,!0);a.timerscroll&&a.timerscroll.tm&&clearInterval(a.timerscroll.tm);0<h&&(!a.scrollrunning&&a.onscrollstart&&a.onscrollstart.call(a,{type:"scrollstart",current:{x:f,y:d},request:{x:b,y:c},end:{x:a.newscrollx,y:a.newscrolly},speed:h}),e.transitionend?a.scrollendtrapped||(a.scrollendtrapped=!0,a.bind(a.doc,e.transitionend,a.onScrollTransitionEnd,!1)):(a.scrollendtrapped&&clearTimeout(a.scrollendtrapped),a.scrollendtrapped=
-setTimeout(a.onScrollTransitionEnd,h)),a.timerscroll={bz:new A(d,a.newscrolly,h,0,0,.58,1),bh:new A(f,a.newscrollx,h,0,0,.58,1)},a.cursorfreezed||(a.timerscroll.tm=setInterval(function(){a.showCursor(a.getScrollTop(),a.getScrollLeft())},60)));a.synched("doScroll-set",function(){a.timer=0;a.scrollendtrapped&&(a.scrollrunning=!0);a.setScrollTop(a.newscrolly);a.setScrollLeft(a.newscrollx);if(!a.scrollendtrapped)a.onScrollTransitionEnd()})},50)},this.cancelScroll=function(){if(!a.scrollendtrapped)return!0;
-var b=a.getScrollTop(),c=a.getScrollLeft();a.scrollrunning=!1;e.transitionend||clearTimeout(e.transitionend);a.scrollendtrapped=!1;a._unbind(a.doc[0],e.transitionend,a.onScrollTransitionEnd);a.prepareTransition(0);a.setScrollTop(b);a.railh&&a.setScrollLeft(c);a.timerscroll&&a.timerscroll.tm&&clearInterval(a.timerscroll.tm);a.timerscroll=!1;a.cursorfreezed=!1;a.showCursor(b,c);return a},this.onScrollTransitionEnd=function(){a.scrollendtrapped&&a._unbind(a.doc[0],e.transitionend,a.onScrollTransitionEnd);
-a.scrollendtrapped=!1;a.prepareTransition(0);a.timerscroll&&a.timerscroll.tm&&clearInterval(a.timerscroll.tm);a.timerscroll=!1;var b=a.getScrollTop(),c=a.getScrollLeft();a.setScrollTop(b);a.railh&&a.setScrollLeft(c);a.noticeCursor(!1,b,c);a.cursorfreezed=!1;0>b?b=0:b>a.page.maxh&&(b=a.page.maxh);0>c?c=0:c>a.page.maxw&&(c=a.page.maxw);if(b!=a.newscrolly||c!=a.newscrollx)return a.doScrollPos(c,b,a.opt.snapbackspeed);a.onscrollend&&a.scrollrunning&&a.triggerScrollEnd();a.scrollrunning=!1}):(this.doScrollLeft=
-function(b,c){var d=a.scrollrunning?a.newscrolly:a.getScrollTop();a.doScrollPos(b,d,c)},this.doScrollTop=function(b,c){var d=a.scrollrunning?a.newscrollx:a.getScrollLeft();a.doScrollPos(d,b,c)},this.doScrollPos=function(b,c,d){function e(){if(a.cancelAnimationFrame)return!0;a.scrollrunning=!0;if(n=1-n)return a.timer=s(e)||1;var b=0,c,d,g=d=a.getScrollTop();if(a.dst.ay){g=a.bzscroll?a.dst.py+a.bzscroll.getNow()*a.dst.ay:a.newscrolly;c=g-d;if(0>c&&g<a.newscrolly||0<c&&g>a.newscrolly)g=a.newscrolly;
-a.setScrollTop(g);g==a.newscrolly&&(b=1)}else b=1;d=c=a.getScrollLeft();if(a.dst.ax){d=a.bzscroll?a.dst.px+a.bzscroll.getNow()*a.dst.ax:a.newscrollx;c=d-c;if(0>c&&d<a.newscrollx||0<c&&d>a.newscrollx)d=a.newscrollx;a.setScrollLeft(d);d==a.newscrollx&&(b+=1)}else b+=1;2==b?(a.timer=0,a.cursorfreezed=!1,a.bzscroll=!1,a.scrollrunning=!1,0>g?g=0:g>a.page.maxh&&(g=a.page.maxh),0>d?d=0:d>a.page.maxw&&(d=a.page.maxw),d!=a.newscrollx||g!=a.newscrolly?a.doScrollPos(d,g):a.onscrollend&&a.triggerScrollEnd()):
-a.timer=s(e)||1}c="undefined"==typeof c||!1===c?a.getScrollTop(!0):c;if(a.timer&&a.newscrolly==c&&a.newscrollx==b)return!0;a.timer&&t(a.timer);a.timer=0;var f=a.getScrollTop(),h=a.getScrollLeft();(0>(a.newscrolly-f)*(c-f)||0>(a.newscrollx-h)*(b-h))&&a.cancelScroll();a.newscrolly=c;a.newscrollx=b;a.bouncescroll&&a.rail.visibility||(0>a.newscrolly?a.newscrolly=0:a.newscrolly>a.page.maxh&&(a.newscrolly=a.page.maxh));a.bouncescroll&&a.railh.visibility||(0>a.newscrollx?a.newscrollx=0:a.newscrollx>a.page.maxw&&
-(a.newscrollx=a.page.maxw));a.dst={};a.dst.x=b-h;a.dst.y=c-f;a.dst.px=h;a.dst.py=f;var k=Math.round(Math.sqrt(Math.pow(a.dst.x,2)+Math.pow(a.dst.y,2)));a.dst.ax=a.dst.x/k;a.dst.ay=a.dst.y/k;var l=0,m=k;0==a.dst.x?(l=f,m=c,a.dst.ay=1,a.dst.py=0):0==a.dst.y&&(l=h,m=b,a.dst.ax=1,a.dst.px=0);k=a.getTransitionSpeed(k);d&&1>=d&&(k*=d);a.bzscroll=0<k?a.bzscroll?a.bzscroll.update(m,k):new A(l,m,k,0,1,0,1):!1;if(!a.timer){(f==a.page.maxh&&c>=a.page.maxh||h==a.page.maxw&&b>=a.page.maxw)&&a.checkContentSize();
-var n=1;a.cancelAnimationFrame=!1;a.timer=1;a.onscrollstart&&!a.scrollrunning&&a.onscrollstart.call(a,{type:"scrollstart",current:{x:h,y:f},request:{x:b,y:c},end:{x:a.newscrollx,y:a.newscrolly},speed:k});e();(f==a.page.maxh&&c>=f||h==a.page.maxw&&b>=h)&&a.checkContentSize();a.noticeCursor()}},this.cancelScroll=function(){a.timer&&t(a.timer);a.timer=0;a.bzscroll=!1;a.scrollrunning=!1;return a}):(this.doScrollLeft=function(b,c){var d=a.getScrollTop();a.doScrollPos(b,d,c)},this.doScrollTop=function(b,
-c){var d=a.getScrollLeft();a.doScrollPos(d,b,c)},this.doScrollPos=function(b,c,d){var e=b>a.page.maxw?a.page.maxw:b;0>e&&(e=0);var f=c>a.page.maxh?a.page.maxh:c;0>f&&(f=0);a.synched("scroll",function(){a.setScrollTop(f);a.setScrollLeft(e)})},this.cancelScroll=function(){});this.doScrollBy=function(b,c){var d=0,d=c?Math.floor((a.scroll.y-b)*a.scrollratio.y):(a.timer?a.newscrolly:a.getScrollTop(!0))-b;if(a.bouncescroll){var e=Math.round(a.view.h/2);d<-e?d=-e:d>a.page.maxh+e&&(d=a.page.maxh+e)}a.cursorfreezed=
-!1;e=a.getScrollTop(!0);if(0>d&&0>=e)return a.noticeCursor();if(d>a.page.maxh&&e>=a.page.maxh)return a.checkContentSize(),a.noticeCursor();a.doScrollTop(d)};this.doScrollLeftBy=function(b,c){var d=0,d=c?Math.floor((a.scroll.x-b)*a.scrollratio.x):(a.timer?a.newscrollx:a.getScrollLeft(!0))-b;if(a.bouncescroll){var e=Math.round(a.view.w/2);d<-e?d=-e:d>a.page.maxw+e&&(d=a.page.maxw+e)}a.cursorfreezed=!1;e=a.getScrollLeft(!0);if(0>d&&0>=e||d>a.page.maxw&&e>=a.page.maxw)return a.noticeCursor();a.doScrollLeft(d)};
-this.doScrollTo=function(b,c){c&&Math.round(b*a.scrollratio.y);a.cursorfreezed=!1;a.doScrollTop(b)};this.checkContentSize=function(){var b=a.getContentSize();b.h==a.page.h&&b.w==a.page.w||a.resize(!1,b)};a.onscroll=function(b){a.rail.drag||a.cursorfreezed||a.synched("scroll",function(){a.scroll.y=Math.round(a.getScrollTop()*(1/a.scrollratio.y));a.railh&&(a.scroll.x=Math.round(a.getScrollLeft()*(1/a.scrollratio.x)));a.noticeCursor()})};a.bind(a.docscroll,"scroll",a.onscroll);this.doZoomIn=function(b){if(!a.zoomactive){a.zoomactive=
-!0;a.zoomrestore={style:{}};var c="position top left zIndex backgroundColor marginTop marginBottom marginLeft marginRight".split(" "),d=a.win[0].style,h;for(h in c){var k=c[h];a.zoomrestore.style[k]="undefined"!=typeof d[k]?d[k]:""}a.zoomrestore.style.width=a.win.css("width");a.zoomrestore.style.height=a.win.css("height");a.zoomrestore.padding={w:a.win.outerWidth()-a.win.width(),h:a.win.outerHeight()-a.win.height()};e.isios4&&(a.zoomrestore.scrollTop=f(window).scrollTop(),f(window).scrollTop(0));
-a.win.css({position:e.isios4?"absolute":"fixed",top:0,left:0,"z-index":x+100,margin:"0px"});c=a.win.css("backgroundColor");(""==c||/transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(c))&&a.win.css("backgroundColor","#fff");a.rail.css({"z-index":x+101});a.zoom.css({"z-index":x+102});a.zoom.css("backgroundPosition","0px -18px");a.resizeZoom();a.onzoomin&&a.onzoomin.call(a);return a.cancelEvent(b)}};this.doZoomOut=function(b){if(a.zoomactive)return a.zoomactive=!1,a.win.css("margin",""),a.win.css(a.zoomrestore.style),
-e.isios4&&f(window).scrollTop(a.zoomrestore.scrollTop),a.rail.css({"z-index":a.zindex}),a.zoom.css({"z-index":a.zindex}),a.zoomrestore=!1,a.zoom.css("backgroundPosition","0px 0px"),a.onResize(),a.onzoomout&&a.onzoomout.call(a),a.cancelEvent(b)};this.doZoom=function(b){return a.zoomactive?a.doZoomOut(b):a.doZoomIn(b)};this.resizeZoom=function(){if(a.zoomactive){var b=a.getScrollTop();a.win.css({width:f(window).width()-a.zoomrestore.padding.w+"px",height:f(window).height()-a.zoomrestore.padding.h+"px"});
-a.onResize();a.setScrollTop(Math.min(a.page.maxh,b))}};this.init();f.nicescroll.push(this)},L=function(f){var c=this;this.nc=f;this.steptime=this.lasttime=this.speedy=this.speedx=this.lasty=this.lastx=0;this.snapy=this.snapx=!1;this.demuly=this.demulx=0;this.lastscrolly=this.lastscrollx=-1;this.timer=this.chky=this.chkx=0;this.time=function(){return+new Date};this.reset=function(f,k){c.stop();var d=c.time();c.steptime=0;c.lasttime=d;c.speedx=0;c.speedy=0;c.lastx=f;c.lasty=k;c.lastscrollx=-1;c.lastscrolly=
--1};this.update=function(f,k){var d=c.time();c.steptime=d-c.lasttime;c.lasttime=d;var d=k-c.lasty,n=f-c.lastx,p=c.nc.getScrollTop(),a=c.nc.getScrollLeft(),p=p+d,a=a+n;c.snapx=0>a||a>c.nc.page.maxw;c.snapy=0>p||p>c.nc.page.maxh;c.speedx=n;c.speedy=d;c.lastx=f;c.lasty=k};this.stop=function(){c.nc.unsynched("domomentum2d");c.timer&&clearTimeout(c.timer);c.timer=0;c.lastscrollx=-1;c.lastscrolly=-1};this.doSnapy=function(f,k){var d=!1;0>k?(k=0,d=!0):k>c.nc.page.maxh&&(k=c.nc.page.maxh,d=!0);0>f?(f=0,d=
-!0):f>c.nc.page.maxw&&(f=c.nc.page.maxw,d=!0);d?c.nc.doScrollPos(f,k,c.nc.opt.snapbackspeed):c.nc.triggerScrollEnd()};this.doMomentum=function(f){var k=c.time(),d=f?k+f:c.lasttime;f=c.nc.getScrollLeft();var n=c.nc.getScrollTop(),p=c.nc.page.maxh,a=c.nc.page.maxw;c.speedx=0<a?Math.min(60,c.speedx):0;c.speedy=0<p?Math.min(60,c.speedy):0;d=d&&60>=k-d;if(0>n||n>p||0>f||f>a)d=!1;f=c.speedx&&d?c.speedx:!1;if(c.speedy&&d&&c.speedy||f){var s=Math.max(16,c.steptime);50<s&&(f=s/50,c.speedx*=f,c.speedy*=f,s=
-50);c.demulxy=0;c.lastscrollx=c.nc.getScrollLeft();c.chkx=c.lastscrollx;c.lastscrolly=c.nc.getScrollTop();c.chky=c.lastscrolly;var e=c.lastscrollx,r=c.lastscrolly,t=function(){var d=600<c.time()-k?.04:.02;c.speedx&&(e=Math.floor(c.lastscrollx-c.speedx*(1-c.demulxy)),c.lastscrollx=e,0>e||e>a)&&(d=.1);c.speedy&&(r=Math.floor(c.lastscrolly-c.speedy*(1-c.demulxy)),c.lastscrolly=r,0>r||r>p)&&(d=.1);c.demulxy=Math.min(1,c.demulxy+d);c.nc.synched("domomentum2d",function(){c.speedx&&(c.nc.getScrollLeft()!=
-c.chkx&&c.stop(),c.chkx=e,c.nc.setScrollLeft(e));c.speedy&&(c.nc.getScrollTop()!=c.chky&&c.stop(),c.chky=r,c.nc.setScrollTop(r));c.timer||(c.nc.hideCursor(),c.doSnapy(e,r))});1>c.demulxy?c.timer=setTimeout(t,s):(c.stop(),c.nc.hideCursor(),c.doSnapy(e,r))};t()}else c.doSnapy(c.nc.getScrollLeft(),c.nc.getScrollTop())}},w=f.fn.scrollTop;f.cssHooks.pageYOffset={get:function(k,c,h){return(c=f.data(k,"__nicescroll")||!1)&&c.ishwscroll?c.getScrollTop():w.call(k)},set:function(k,c){var h=f.data(k,"__nicescroll")||
-!1;h&&h.ishwscroll?h.setScrollTop(parseInt(c)):w.call(k,c);return this}};f.fn.scrollTop=function(k){if("undefined"==typeof k){var c=this[0]?f.data(this[0],"__nicescroll")||!1:!1;return c&&c.ishwscroll?c.getScrollTop():w.call(this)}return this.each(function(){var c=f.data(this,"__nicescroll")||!1;c&&c.ishwscroll?c.setScrollTop(parseInt(k)):w.call(f(this),k)})};var B=f.fn.scrollLeft;f.cssHooks.pageXOffset={get:function(k,c,h){return(c=f.data(k,"__nicescroll")||!1)&&c.ishwscroll?c.getScrollLeft():B.call(k)},
-set:function(k,c){var h=f.data(k,"__nicescroll")||!1;h&&h.ishwscroll?h.setScrollLeft(parseInt(c)):B.call(k,c);return this}};f.fn.scrollLeft=function(k){if("undefined"==typeof k){var c=this[0]?f.data(this[0],"__nicescroll")||!1:!1;return c&&c.ishwscroll?c.getScrollLeft():B.call(this)}return this.each(function(){var c=f.data(this,"__nicescroll")||!1;c&&c.ishwscroll?c.setScrollLeft(parseInt(k)):B.call(f(this),k)})};var C=function(k){var c=this;this.length=0;this.name="nicescrollarray";this.each=function(d){for(var f=
-0,h=0;f<c.length;f++)d.call(c[f],h++);return c};this.push=function(d){c[c.length]=d;c.length++};this.eq=function(d){return c[d]};if(k)for(var h=0;h<k.length;h++){var m=f.data(k[h],"__nicescroll")||!1;m&&(this[this.length]=m,this.length++)}return this};(function(f,c,h){for(var m=0;m<c.length;m++)h(f,c[m])})(C.prototype,"show hide toggle onResize resize remove stop doScrollPos".split(" "),function(f,c){f[c]=function(){var f=arguments;return this.each(function(){this[c].apply(this,f)})}});f.fn.getNiceScroll=
-function(k){return"undefined"==typeof k?new C(this):this[k]&&f.data(this[k],"__nicescroll")||!1};f.extend(f.expr[":"],{nicescroll:function(k){return f.data(k,"__nicescroll")?!0:!1}});f.fn.niceScroll=function(k,c){"undefined"!=typeof c||"object"!=typeof k||"jquery"in k||(c=k,k=!1);c=f.extend({},c);var h=new C;"undefined"==typeof c&&(c={});k&&(c.doc=f(k),c.win=f(this));var m=!("doc"in c);m||"win"in c||(c.win=f(this));this.each(function(){var d=f(this).data("__nicescroll")||!1;d||(c.doc=m?f(this):c.doc,
-d=new R(c,f(this)),f(this).data("__nicescroll",d));h.push(d)});return 1==h.length?h[0]:h};window.NiceScroll={getjQuery:function(){return f}};f.nicescroll||(f.nicescroll=new C,f.nicescroll.options=I)});
diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js
new file mode 100644
index 00000000000..da37966b28a
--- /dev/null
+++ b/vendor/assets/javascripts/latinise.js
@@ -0,0 +1,11 @@
+// Converting text to basic latin (aka removing accents)
+//
+// Based on: http://semplicewebsites.com/removing-accents-javascript
+//
+var Latinise = {
+ map: {"Á":"A","Ă":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ǎ":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","Ạ":"A","Ȁ":"A","À":"A","Ả":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Ɓ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","Ḉ":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","Ḑ":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","Ɗ":"D","Ḏ":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ė":"E","Ẹ":"E","Ȅ":"E","È":"E","Ẻ":"E","Ȇ":"E","Ē":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","Ꝫ":"ET","Ḟ":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Ȟ":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","Ḯ":"I","İ":"I","Ị":"I","Ȉ":"I","Ì":"I","Ỉ":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","Ḭ":"I","Ꝺ":"D","Ꝼ":"F","Ᵹ":"G","Ꞃ":"R","Ꞅ":"S","Ꞇ":"T","Ꝭ":"IS","Ĵ":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","Ꝃ":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","Ꝁ":"K","Ꝅ":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","Ⱡ":"L","Ꝉ":"L","Ḻ":"L","Ŀ":"L","Ɫ":"L","Lj":"L","Ł":"L","LJ":"LJ","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ń":"N","Ň":"N","Ņ":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Ɲ":"N","Ṉ":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","Ố":"O","Ộ":"O","Ồ":"O","Ổ":"O","Ỗ":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","Ọ":"O","Ő":"O","Ȍ":"O","Ò":"O","Ỏ":"O","Ơ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","Ỡ":"O","Ȏ":"O","Ꝋ":"O","Ꝍ":"O","Ō":"O","Ṓ":"O","Ṑ":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Ƣ":"OI","Ꝏ":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","Ṕ":"P","Ṗ":"P","Ꝓ":"P","Ƥ":"P","Ꝕ":"P","Ᵽ":"P","Ꝑ":"P","Ꝙ":"Q","Ꝗ":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","Ȑ":"R","Ȓ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꜿ":"C","Ǝ":"E","Ś":"S","Ṥ":"S","Š":"S","Ṧ":"S","Ş":"S","Ŝ":"S","Ș":"S","Ṡ":"S","Ṣ":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Ţ":"T","Ṱ":"T","Ț":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","Ṯ":"T","Ʈ":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Ɯ":"M","Ʌ":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","Ṷ":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","Ṳ":"U","Ụ":"U","Ű":"U","Ȕ":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","Ự":"U","Ừ":"U","Ử":"U","Ữ":"U","Ȗ":"U","Ū":"U","Ṻ":"U","Ų":"U","Ů":"U","Ũ":"U","Ṹ":"U","Ṵ":"U","Ꝟ":"V","Ṿ":"V","Ʋ":"V","Ṽ":"V","Ꝡ":"VY","Ẃ":"W","Ŵ":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","Ⱳ":"W","Ẍ":"X","Ẋ":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","Ỵ":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","Ɏ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","Ẑ":"Z","Ⱬ":"Z","Ż":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","ᴀ":"A","ᴁ":"AE","ʙ":"B","ᴃ":"B","ᴄ":"C","ᴅ":"D","ᴇ":"E","ꜰ":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","ᴊ":"J","ᴋ":"K","ʟ":"L","ᴌ":"L","ᴍ":"M","ɴ":"N","ᴏ":"O","ɶ":"OE","ᴐ":"O","ᴕ":"OU","ᴘ":"P","ʀ":"R","ᴎ":"N","ᴙ":"R","ꜱ":"S","ᴛ":"T","ⱻ":"E","ᴚ":"R","ᴜ":"U","ᴠ":"V","ᴡ":"W","ʏ":"Y","ᴢ":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ǎ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","ạ":"a","ȁ":"a","à":"a","ả":"a","ȃ":"a","ā":"a","ą":"a","ᶏ":"a","ẚ":"a","å":"a","ǻ":"a","ḁ":"a","ⱥ":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","ɓ":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","ḉ":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","ḑ":"d","ḓ":"d","ȡ":"d","ḋ":"d","ḍ":"d","ɗ":"d","ᶑ":"d","ḏ":"d","ᵭ":"d","ᶁ":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","ḝ":"e","ê":"e","ế":"e","ệ":"e","ề":"e","ể":"e","ễ":"e","ḙ":"e","ë":"e","ė":"e","ẹ":"e","ȅ":"e","è":"e","ẻ":"e","ȇ":"e","ē":"e","ḗ":"e","ḕ":"e","ⱸ":"e","ę":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ꝫ":"et","ḟ":"f","ƒ":"f","ᵮ":"f","ᶂ":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","ḡ":"g","ᶃ":"g","ǥ":"g","ḫ":"h","ȟ":"h","ḩ":"h","ĥ":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","ȋ":"i","ī":"i","į":"i","ᶖ":"i","ɨ":"i","ĩ":"i","ḭ":"i","ꝺ":"d","ꝼ":"f","ᵹ":"g","ꞃ":"r","ꞅ":"s","ꞇ":"t","ꝭ":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","ḱ":"k","ǩ":"k","ķ":"k","ⱪ":"k","ꝃ":"k","ḳ":"k","ƙ":"k","ḵ":"k","ᶄ":"k","ꝁ":"k","ꝅ":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","ȴ":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ꝉ":"l","ḻ":"l","ŀ":"l","ɫ":"l","ᶅ":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","ẜ":"s","ẛ":"s","ẝ":"s","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","ń":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","ṅ":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","ƞ":"n","ᵰ":"n","ᶇ":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","ố":"o","ộ":"o","ồ":"o","ổ":"o","ỗ":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","ọ":"o","ő":"o","ȍ":"o","ò":"o","ỏ":"o","ơ":"o","ớ":"o","ợ":"o","ờ":"o","ở":"o","ỡ":"o","ȏ":"o","ꝋ":"o","ꝍ":"o","ⱺ":"o","ō":"o","ṓ":"o","ṑ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","ṍ":"o","ṏ":"o","ȭ":"o","ƣ":"oi","ꝏ":"oo","ɛ":"e","ᶓ":"e","ɔ":"o","ᶗ":"o","ȣ":"ou","ṕ":"p","ṗ":"p","ꝓ":"p","ƥ":"p","ᵱ":"p","ᶈ":"p","ꝕ":"p","ᵽ":"p","ꝑ":"p","ꝙ":"q","ʠ":"q","ɋ":"q","ꝗ":"q","ŕ":"r","ř":"r","ŗ":"r","ṙ":"r","ṛ":"r","ṝ":"r","ȑ":"r","ɾ":"r","ᵳ":"r","ȓ":"r","ṟ":"r","ɼ":"r","ᵲ":"r","ᶉ":"r","ɍ":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","ɿ":"r","ś":"s","ṥ":"s","š":"s","ṧ":"s","ş":"s","ŝ":"s","ș":"s","ṡ":"s","ṣ":"s","ṩ":"s","ʂ":"s","ᵴ":"s","ᶊ":"s","ȿ":"s","ɡ":"g","ß":"ss","ᴑ":"o","ᴓ":"o","ᴝ":"u","ť":"t","ţ":"t","ṱ":"t","ț":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","ṭ":"t","ƭ":"t","ṯ":"t","ᵵ":"t","ƫ":"t","ʈ":"t","ŧ":"t","ᵺ":"th","ɐ":"a","ᴂ":"ae","ǝ":"e","ᵷ":"g","ɥ":"h","ʮ":"h","ʯ":"h","ᴉ":"i","ʞ":"k","ꞁ":"l","ɯ":"m","ɰ":"m","ᴔ":"oe","ɹ":"r","ɻ":"r","ɺ":"r","ⱹ":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","ꜩ":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","ṷ":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","ṳ":"u","ụ":"u","ű":"u","ȕ":"u","ù":"u","ủ":"u","ư":"u","ứ":"u","ự":"u","ừ":"u","ử":"u","ữ":"u","ȗ":"u","ū":"u","ṻ":"u","ų":"u","ᶙ":"u","ů":"u","ũ":"u","ṹ":"u","ṵ":"u","ᵫ":"ue","ꝸ":"um","ⱴ":"v","ꝟ":"v","ṿ":"v","ʋ":"v","ᶌ":"v","ⱱ":"v","ṽ":"v","ꝡ":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","ẁ":"w","ⱳ":"w","ẘ":"w","ẍ":"x","ẋ":"x","ᶍ":"x","ý":"y","ŷ":"y","ÿ":"y","ẏ":"y","ỵ":"y","ỳ":"y","ƴ":"y","ỷ":"y","ỿ":"y","ȳ":"y","ẙ":"y","ɏ":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","ʑ":"z","ⱬ":"z","ż":"z","ẓ":"z","ȥ":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","ʐ":"z","ƶ":"z","ɀ":"z","ff":"ff","ffi":"ffi","ffl":"ffl","fi":"fi","fl":"fl","ij":"ij","œ":"oe","st":"st","ₐ":"a","ₑ":"e","ᵢ":"i","ⱼ":"j","ₒ":"o","ᵣ":"r","ᵤ":"u","ᵥ":"v","ₓ":"x"}
+};
+
+String.prototype.latinise = function() {
+ return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
+};